stevenmaguire / oauth2-keycloak

Keycloak Provider for OAuth 2.0 Client
MIT License
204 stars 151 forks source link

Token verification failed - Setup with knpuniversity/oauth2-client-bundle, docker compose and Traefik v2 #43

Open ToshY opened 2 years ago

ToshY commented 2 years ago

Problem

Has anyone got this package working locally with Docker Compose and Traefik v2? Because I've tried setting this up with the knpuniversity/oauth2-client-bundle, but I keep getting the error that the token verification failed.

IdentityProviderException
HTTP 500 Internal Server Error
invalid_token: Token verification failed

image

Application

I've setup the following in my Symfony 6 (PHP 8.1) app:

config\packages\framework.yaml

  session:
    enabled: true
    handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
    cookie_secure: auto
    cookie_samesite: lax

config\packages\knpu_oauth2_client.yaml

knpu_oauth2_client:
  http_client_options:
    timeout: 0
    proxy: 'http://keycloak:8080'
    verify: false
  clients:
    keycloak:
      type: keycloak
      client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%'
      client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%'
      redirect_route: '%env(OAUTH_KEYCLOAK_REDIRECT_ROUTE)%'
      redirect_params: { }
      auth_server_url: '%env(OAUTH_KEYCLOAK_URL)%'
      realm: '%env(OAUTH_KEYCLOAK_REALM)%'

config\packages\security.yaml

...
  providers:
    oauth:
      id: knpu.oauth2.user_provider
  firewalls:
    dev:
      pattern: ^/(_(profiler|wdt)|css|images|js)/
      security: false
    main:
      lazy: true
      provider: oauth
      custom_authenticator: App\Security\KeycloakAuthenticator

App\Controller\KeycloakController

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Class KeycloakController
 */
final class KeycloakController extends AbstractController
{
    #[Route('/connect/keycloak', name: 'connect_keycloak_start')]
    public function connectAction(
        ClientRegistry $clientRegistry
    ): RedirectResponse {
        return $clientRegistry->getClient('keycloak')->redirect(['email'], []);
    }

    #[Route('/connect/keycloak/check', name: 'connect_keycloak_check')]
    public function connectCheckAction(
        Request $request,
        ClientRegistry $clientRegistry
    ) {
    }
}

App\Security\KeycloakAuthenticator

namespace App\Security;

use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\Provider\KeycloakClient;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

/**
 * Class KeycloakAuthenticator
 */
class KeycloakAuthenticator extends OAuth2Authenticator
{
    private $clientRegistry;
    private $entityManager;
    private $router;

    /**
     * KeycloakAuthenticator constructor.
     * @param  ClientRegistry  $clientRegistry
     * @param  EntityManagerInterface  $em
     * @param  RouterInterface  $router
     */
    public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
    {
        $this->clientRegistry = $clientRegistry;
        $this->entityManager = $em;
        $this->router = $router;
    }

    /**
     * @param  Request  $request
     * @return bool|null
     */
    public function supports(Request $request): ?bool
    {
        return $request->attributes->get('_route') === 'connect_keycloak_check';
    }

    /**
     * @param  Request  $request
     * @return Passport
     */
    public function authenticate(Request $request): Passport
    {
        /** @var KeycloakClient $client */
        $client = $this->clientRegistry->getClient('keycloak');
        $accessToken = $this->fetchAccessToken($client);

        return new SelfValidatingPassport(
            new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
                return $client->fetchUserFromToken($accessToken);
            })
        );
    }

    /**
     * @param  Request  $request
     * @param  TokenInterface  $token
     * @param  string  $firewallName
     * @return Response|null
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        return new RedirectResponse($this->router->generate('admin'));
    }

    /**
     * @param  Request  $request
     * @param  AuthenticationException  $exception
     * @return Response|null
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }
}

Docker Compose

docker-compose.yml - app

  phpfpm:
    build:
      context: docker/php
      dockerfile: Dockerfile
      args:
        PHP_VERSION: ${PHP_VERSION}
    container_name: app_phpfpm
    user: ${UID}:${GID}
    security_opt:
      - no-new-privileges:true
    restart: always
    environment:
      - PHP_OPCACHE_ENABLE=${OPCACHE_ENABLE}
      - PHP_OPCACHE_VALIDATE_TIMESTAMPS=${OPCACHE_VALIDATE_TIMESTAMPS}
      - PHP_OPCACHE_PRELOAD_ENV=${APP_ENV}
    volumes:
      - .:/app
      - ~/certs:/certs
      - ./docker/php/conf/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini
      - ./docker/php/conf/php.ini:/usr/local/etc/php/conf.d/php.ini
      - ./docker/php/conf/www.conf:/usr/local/etc/php-fpm.d/www.${APP_DOMAIN}.conf
    networks:
      - proxy
      - app-network

  apache:
    build:
      context: docker/apache
      dockerfile: Dockerfile
      args:
        APACHE_VERSION: ${APACHE_VERSION}
    container_name: app_apache
    security_opt:
      - no-new-privileges:true
    restart: always
    volumes:
      - .:/app
      - apache_log:/var/log/apache2
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${APP_NAME}-apache-secure.entrypoints=websecure"
      - "traefik.http.routers.${APP_NAME}-apache-secure.rule=Host(`${APP_DOMAIN}`, `www.${APP_DOMAIN}`)"
      - "traefik.http.routers.${APP_NAME}-apache-secure.service=${APP_NAME}-apache-service"
      - "traefik.http.routers.${APP_NAME}-apache-secure.tls=true"
      - "traefik.http.routers.${APP_NAME}-apache-secure.middlewares=secure-headers@file"
      - "traefik.http.services.${APP_NAME}-apache-service.loadbalancer.server.port=80"
      - "traefik.docker.network=proxy"
    networks:
      - proxy
      - app-network

docker-compose.yml - keycloak (16.1.0)

  keycloak:
    image: jboss/keycloak:${KEYCLOAK_VERSION}
    container_name: keycloak
    depends_on:
      - keycloak-postgres
    environment:
      DB_VENDOR: postgres
      DB_ADDR: keycloak_postgres
      DB_PORT: 5432
      DB_DATABASE: ${KEYCLOAK_STORAGE_POSTGRES_DATABASE}
      DB_USER: ${KEYCLOAK_STORAGE_POSTGRES_USERNAME}
      DB_PASSWORD: ${KEYCLOAK_STORAGE_POSTGRES_PASSWORD}
      KEYCLOAK_USER: ${KEYCLOAK_USERNAME}
      KEYCLOAK_PASSWORD: ${KEYCLOAK_PASSWORD}
      KEYCLOAK_HOSTNAME: keycloak.${APP_DOMAIN}
      PROXY_ADDRESS_FORWARDING: 'true'
      KEYCLOAK_LOGLEVEL: DEBUG
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.keycloak-secure.entrypoints=websecure"
      - "traefik.http.routers.keycloak-secure.rule=Host(`keycloak.${APP_DOMAIN}`, `www.keycloak.${APP_DOMAIN}`)"
      - "traefik.http.routers.keycloak-secure.service=keycloak-service"
      - "traefik.http.routers.keycloak-secure.tls=true"
      - "traefik.http.services.keycloak-service.loadbalancer.passhostheader=true"
      - "traefik.http.services.keycloak-service.loadbalancer.server.port=8080"
      - "traefik.docker.network=proxy"
    networks:
      - proxy
      - keycloak-network

docker-compose.yml - traefik (2.5)

  traefik:
    image: traefik:v${TRAEFIK_VERSION}
    container_name: traefik
    security_opt:
      - no-new-privileges:true
    ports:
      - 80:80
      - 443:443
      - 8080:8080
    restart: always
    environment:
      CF_API_EMAIL: ${CF_EMAIL}
      CF_API_KEY: ${CF_API_KEY}
    volumes:
      - ./docker/traefik/config/traefik.yml:/traefik.yml:ro
      - ./docker/traefik/config/dynamic:/dynamic:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /etc/localtime:/etc/localtime:ro
      - /usr/share/zoneinfo:/usr/share/zoneinfo:ro
      - ~/certs:/certs
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik-secure.entrypoints=websecure"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.${APP_DOMAIN}`, `www.traefik.${APP_DOMAIN}`)"
      - "traefik.http.routers.traefik-secure.service=api@internal"
      - "traefik.http.routers.traefik-secure.tls=true"
      - "traefik.docker.network=proxy"

traefik.yml

api:
  dashboard: true

log:
  format: json
  level: DEBUG

entryPoints:
  web:
    address: :80
    http:
      redirections:
        entryPoint:
          to: websecure

  websecure:
    address: :443

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    watch: true
    network: proxy
  file:
    directory: /dynamic
    watch: true

dynamic/http.yml

http:
  middlewares:
    secure-headers:
      headers:
        frameDeny: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 63072000
        customFrameOptionsValue: SAMEORIGIN
        referrerPolicy: "strict-origin-when-cross-origin"
        customRequestHeaders:
          X-Forwarded-Proto: https
          X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex,"

with proxy set as external network.

Keycloak

I've setup the client as follows and configured the Credentials with basic Client Id and Secret (no signed JWT).

image

When checking the events, only thing that seems different is that the first the IP address being used. The LOGIN action uses the client IP, whereas the CODE_TO_TOKEN action uses the IP of the docker container. image

No idea if this is related to the token verification failing.

Debug

After trying to debug the authentication progress, I noticed that the access_token that is returned after login, is the same as the one being used to get the users info. So it hasn't changed or anything (which I could image causing the error).

It's already been 3 days of (trying) to debug this but there seems literally no similar cases to be found anywhere, so I'm out of ideas.

dinooo13 commented 2 years ago

Hey @ToshY, I'm not 100% sure but I think your proxy setting should point to your traefik instance like 'traefik:80' so you only communicate via the public url with keycloak. If this isn't helping feel free to contact me, I got a working project (php:8.1, symfony:6, keycloak:latest, traefik:2.6) but can't share it here because it's from my workplace.

tsdevelopment commented 2 years ago

I had the same issue. The problem here is that the token has a different issuer(iss) value. When you come from outside the "iss" value is "http://localhost:8080/auth/realms/master". But inside the docker container the value becomes "http://keycloak:8080/auth/realms/master".

I used my local network address (http://192.168.138.113) and everything works like a charm:

knpu_oauth2_client:
    clients:
        keycloak:
            type: keycloak
            client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%'
            client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%'
            redirect_route: security_oauth_keycloak_redirect
            auth_server_url: 'http://192.168.138.113:8081/auth'
            realm: '%env(OAUTH_KEYCLOAK_REALM)%'
            encryption_algorithm: RS256
eldarj commented 2 years ago

Can we add multiple allowed auth_server_urls ie. issuers?