Как лучше получить client_credential токен через прокси oauth2-proxy?
Сейчас ситуация такая: сеть настроена через реверс-прокси traefik. Сам траефик не умеет работать с аутентификацией, для этого необходимо прокладывать oauth прокси. Появилась задача внутри сети прокинуть токены в заголовки всех приложений без авторизации пользователя. Keycloak настроен и отдает токен. Проблема появилась при конфигурации oauth2-proxy, он работает как autentification_code то есть аутентификация происходит при вмешательстве пользователя, мне нужно получить токен без вмешательства пользователя, так называемый M2M обмен, токен должен быть передан как client_credentials.
По вводным: конфигурация oauth2-proxy в traefik
oauth2-cred-proxy: image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 container_name: oauth2-cred-proxy environment: OAUTH2_PROXY_LOG_LEVEL: "debug" OAUTH2_PROXY_PROVIDER: "oidc" OAUTH2_PROXY_CLIENT_ID: "@oauth2-cred-proxy" OAUTH2_PROXY_CLIENT_SECRET: "@secret-oauth2-cred-proxy" OAUTH2_PROXY_COOKIE_SECURE: "true" OAUTH2_PROXY_COOKIE_SECRET: "@cookie-oauth2-cred-proxy" OAUTH2_PROXY_COOKIE_REFRESH: "600s" OAUTH2_PROXY_COOKIE_SAMESITE: "none" # Для кросс-доменных запросов OAUTH2_PROXY_COOKIE_DOMAINS: ".example.com" OAUTH2_PROXY_PASS_ACCESS_TOKEN: "true" # Важно: разрешаем прокси передавать токен в заголовке OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: "true" OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY: "true" OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true" OAUTH2_PROXY_UPSTREAMS: static://204 OAUTH2_PROXY_EMAIL_DOMAINS: "*" OAUTH2_PROXY_WHITELIST_DOMAINS: ".example.com" OAUTH2_PROXY_SET_AUTHORIZATION_HEADER: "true" OAUTH2_PROXY_SET_XAUTHREQUEST: "true" OAUTH2_PROXY_CODE_CHALLENGE_METHOD: "S256" OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180" OAUTH2_PROXY_REVERSE_PROXY: "true" OAUTH2_PROXY_ALLOWED_REDIRECT_DOMAINS: ".example.com" OAUTH2_PROXY_SHOW_DEBUG_ON_ERROR: "true" # Позволяет проверять только токены без редиректа OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS: "false" OAUTH2_PROXY_OIDC_PKCE: "false" OAUTH2_PROXY_OIDC_ISSUER_URL: "https://keycloak.example.com/realms/example.com-realm" OAUTH2_PROXY_OIDC_AUTH_URL: "https://keycloak.example.com/realms/example.com-realm/protocol/openid-connect/auth" OAUTH2_PROXY_OIDC_TOKEN_URL: "https://keycloak.example.com/realms/example.com-realm/protocol/openid-connect/token" OAUTH2_PROXY_OIDC_USER_INFO_URL: "https://keycloak.example.com/realms/example.com-realm/protocol/openid-connect/userinfo" expose: - "4180" labels: - "traefik.enable=true" - "traefik.http.routers.oauth2-cred-proxy-secure.rule=Host(`oauth.example.com`)" - "traefik.http.routers.oauth2-cred-proxy-secure.entrypoints=websecure" - "traefik.http.routers.oauth2-cred-proxy-secure.tls=true" - "traefik.http.routers.oauth2-cred-proxy-secure.tls.certresolver=myresolver" # генерация сертификата - "traefik.http.routers.oauth2-cred-proxy-secure.service=oauth2-cred-proxy-secure" - "traefik.http.services.oauth2-cred-proxy-secure.loadbalancer.server.port=4180" networks: - traefik_my-network</blockquote>
Конфигурация keycloak
{ "clientId": "@oauth2-cred-proxy", "name": "@oauth2-cred-proxy", "description": "", "rootUrl": "", "adminUrl": "", "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "@secret-oauth2-cred-proxy", "redirectUris": [ "*" ], "webOrigins": [ "https://keycloak.example.com", "https://echo.example.com", "+" ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": true, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": true, "publicClient": false, "frontchannelLogout": true, "protocol": "openid-connect", "attributes": { "realm_client": "false", "oidc.ciba.grant.enabled": "true", "client.secret.creation.time": "1753858235", "backchannel.logout.session.required": "true", "standard.token.exchange.enabled": "true", "frontchannel.logout.session.required": "true", "oauth2.device.authorization.grant.enabled": "true", "display.on.consent.screen": "false", "pkce.code.challenge.method": "S256", "backchannel.logout.revoke.offline.tokens": "false" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "defaultClientScopes": [ "service_account", "web-origins", "acr", "grant_type_scope", "roles", "profile", "basic", "email" ], "optionalClientScopes": [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ], "access": { "view": true, "configure": true, "manage": true } }
По вводным: запрос токена из keycloak напрямую отдает access_token
docker run --rm curlimages/curl -i -X POST -d "client_id=@oauth2-cred-proxy" -d "client_secret=@secret-oauth2-cred-proxy" -d "grant_type=client_credentials" -d "scope=openid" https://keycloak.example.com/realms/example.com-realm/protocol/openid-connect/token
По вводным: запрос токена из keykloak через oauth2-proxy отдает заголовки без access_token
docker run --rm --network traefik_my-network curlimages/curl curl -i http://oauth2-cred-proxy:4180
root@www:~# docker run --rm --network traefik_my-network curlimages/curl curl -i http://oauth2-cred-proxy:4180 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0HTTP/1.1 302 Found Cache-Control: no-cache, no-store, must-revalidate, max-age=0 Content-Type: text/html; charset=utf-8 Expires: Thu, 01 Jan 1970 00:00:00 UTC Location: https://keycloak.example.com/realms/example.com-realm/protocol/openid-connect/auth?approval_prompt=force&client_id=oauth2-cred-proxy&code_challenge=qldLKr4qsT_idKXnBetxkV4AeY4JBNSvWO9b7bOlX_U&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fkeycloak.example.com%2Frealms%2Fexample.com-realm%2Fprotocol%2Fopenid-connect%2Ftoken&response_type=code&scope=openid+email+profile&state=prEDLzhcPSAgcB6PYBHLzsjnLADT19ARaBotVVABHLo%3A%2F Set-Cookie: _oauth2_proxy_csrf=Op5su9atPad2h1twvnAlzS8F1QCdCIsCvDTRvlqrZX_QH_rCObPxLO2oQ1qrqiDCH4KQB9y9EJfZQGflJt1z7RXJxdGLdbCvdGCkMQvss70K80P4PSKIB66CU_5MoCDPr-sKFSEUyVnF2MOJXHGTaaqJ9nRX9IVGbSBLTuKUwU-ExTDZWAFmPsgxcFiAU3GxZcUmCgtrpmTqH2XDcLOYeb-3_NRpxQG-1g19Gfgt5zkhSc-NmkHN6IvvCEEh8w==|1753892954|TZMWEOiw8wWGWPSJ4ty__tsYiuePhFsaKYl3fkRHjNQ=; Path=/; Domain=example.com; Expires=Wed, 30 Jul 2025 16:44:14 GMT; HttpOnly; Secure; SameSite=None 100 484 100 484 0 0 10437 0 --:--:-- --:--:-- --:--:-- 10521 X-Accel-Expires: 0 Date: Wed, 30 Jul 2025 16:29:14 GMT Content-Length: 484 <a href="https://keycloak.example.com/realms/example.com-realm/protocol/openid-connect/auth?approval_prompt=force&client_id=oauth2-cred-proxy&code_challenge=qldLKr4qsT_idKXnBetxkV4AeY4JBNSvWO9b7bOlX_U&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fkeycloak.example.com%2Frealms%2Fexample.com-realm%2Fprotocol%2Fopenid-connect%2Ftoken&response_type=code&scope=openid+email+profile&state=prEDLzhcPSAgcB6PYBHLzsjnLADT19ARaBotVVABHLo%3A%2F">Found</a>.
По вводным: думаю, для знатоков oauth2-proxy настройка такой конфигурации должна быть простой.
Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.
Пока нет других ответов. Будьте первым, кто поможет автору.
Ответить на вопрос
oauth2-proxy обычно предназначен для интерактивной авторизации пользователя через браузер, а client_credentials - это machine-to-machine сценарий. Поэтому пытаться “получить client_credentials токен через oauth2-proxy” часто архитектурно неверно. oauth2-proxy не должен становиться универсальным token broker для сервисов, если он не рассчитан на такую схему.
Правильная архитектура зависит от цели. Если внутреннему сервису нужен токен от имени приложения, пусть он сам получает client_credentials токен у IdP:
curl -X POST https://idp.example.com/oauth/token -d "grant_type=client_credentials" -d "client_id=service-a" -d "client_secret=secret" -d "scope=api.read"
Если нужно прокидывать данные пользователя за oauth2-proxy, используйте заголовки, которые oauth2-proxy умеет выставлять после пользовательской авторизации: email, user, groups, access token, если это включено в конфигурации.
Для Traefik схема обычно такая:
client -> traefik -> forwardAuth/oauth2-proxy -> app
Приложение получает подтверждённые заголовки только после успешной авторизации пользователя. Но это не то же самое, что client_credentials.
Если у вас несколько внутренних сервисов, лучше разделить:
Итог: client_credentials должен запрашивать backend/service, которому этот токен нужен. oauth2-proxy оставьте для browser login и forwardAuth. Если смешать эти роли, получится сложная и небезопасная схема, где любой внутренний сервис может получить лишний токен.