【问题标题】:Symfony OAuth2 with Keycloak returns invalid_token: Token verification failed带有 Keycloak 的 Symfony OAuth2 返回 invalid_token:令牌验证失败
【发布时间】:2022-01-07 19:47:24
【问题描述】:

目标

我正在尝试使用 Keycloak 和库 knpuniversity/oauth2-client-bundle 和 stevenmaguire/oauth2-keycloak 在 Symfony 6 中设置 OAuth2。一切都是用docker compose (v2.2) 设置的,我使用Traefik (v2.5) 作为代理。

问题

在尝试验证用户时,转到/connect/keycloak,成功重定向到https://keycloak.example.local/auth/realms/.../protocol/openid-connect/auth,成功登录并重定向回/connect/keycloak/check,我不断收到令牌验证失败的错误。

IdentityProviderException
HTTP 500 Internal Server Error
invalid_token: Token verification failed

对底层 guzzle 响应的进一步检查显示 401 statusCode 和 WWW-Authenticate 标头显示错误:

"Bearer realm="redacted", error="invalid_token", error_description="Token verification failed"

应用

我在 Symfony 6 (PHP 8.1) 应用程序中设置了以下内容:

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 编写

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,"

将proxy 设置为外部网络。

钥匙斗篷

我已按如下方式设置客户端并将Credentials 配置为基本Client Id and Secret(无签名JWT)。

检查事件时,唯一看起来不同的是第一个使用的 IP 地址。 LOGIN 操作使用客户端 IP,而 CODE_TO_TOKEN 操作使用 docker 容器的 IP。

不知道这是否与令牌验证失败有关。

会话似乎也可以通过用户 ip 正确设置。

调试

在尝试调试身份验证进度后,我注意到登录后返回的access_token 与用于获取用户信息的access_token 相同。所以它没有改变或任何东西(我可以想象导致错误)。

已经(尝试)调试了 3 天,但我还是无处可去。似乎实际上没有找到可以帮助我进一步调试的类似案例。我不知道我是否遗漏了一些明显的东西,但我现在没有想法。感谢您提供任何帮助。

【问题讨论】:

  • 您找到解决方案了吗?看来我也有同样的问题
  • @VolodymyrI。可悲的是没有。我也在 Keycloak 的github repo 中问过这个问题,但没有得到回复。在mailing list 上询问时,我得到了回复,但我没有投入更多时间来尝试解决此问题。

标签: php symfony docker-compose oauth-2.0


【解决方案1】:

这对我有用。也许这对你也有帮助:https://github.com/stevenmaguire/oauth2-keycloak/issues/43#issuecomment-1066598308

好像是token里面的“issuer”有问题。

【讨论】:

    猜你喜欢
    • 2019-05-18
    • 1970-01-01
    • 2017-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多