Skip to content

feat(auth): OAuth2 Device Authorization Grant login to Grafana - #2

Open
dlddu wants to merge 1 commit into
mainfrom
feat/oauth-pkce-sso-login
Open

feat(auth): OAuth2 Device Authorization Grant login to Grafana#2
dlddu wants to merge 1 commit into
mainfrom
feat/oauth-pkce-sso-login

Conversation

@dlddu

@dlddu dlddu commented Jul 6, 2026

Copy link
Copy Markdown

목적

로컬(및 컨테이너/원격) 환경에서 장기 수명 토큰(Grafana service account token 등)을 걷어내기 위해, mcp-grafana가 SA 토큰 대신 SSO(OAuth2 Device Authorization Grant, RFC 8628)로 로그인해서 단기·사용자별·폐기가능 토큰만 로컬에 두도록 합니다.

  • public client — secret 불필요. 로컬에 client_secret조차 두지 않습니다.
  • Device flow라 브라우저 redirect/loopback 콜백이 없음 → devcontainer·원격·SSH 등 브라우저 없는 환경에서도 동작합니다.
  • 첫 Grafana 요청 시 서버가 device authorization을 수행하고, verification URL + user code를 담은 에러를 반환합니다(→ Claude 채팅에 노출). 사용자가 아무 브라우저에서 승인하면 서버가 백그라운드로 토큰을 폴링해서 획득 → 재시도하면 통과. access token은 Authorization: Bearer로 전송.

upstream(grafana/mcp-grafana) 아니라 이 fork(Buzzvil/mcp-grafana)의 main 대상 draft PR입니다.

왜 device flow인가 (PKCE에서 전환)

처음엔 Authorization Code + PKCE(loopback 콜백)로 구현했으나, devcontainer/원격에서 MCP가 도는 경우 브라우저(호스트)와 콜백 서버(컨테이너)가 다른 네트워크라 loopback 콜백이 닿지 않아 로그인이 완료되지 않았습니다. Device flow는 콜백이 없어 이 문제가 원천적으로 사라집니다.

환경변수

변수 필수 설명
GRAFANA_OAUTH_CLIENT_ID public client ID
GRAFANA_OAUTH_DEVICE_AUTH_URL device authorization endpoint (RFC 8628)
GRAFANA_OAUTH_TOKEN_URL token endpoint
GRAFANA_OAUTH_CLIENT_SECRET confidential client일 때만; public이면 생략
GRAFANA_OAUTH_SCOPES 기본 openid profile email offline_access (offline_access로 refresh token)
GRAFANA_OAUTH_AUDIENCE device endpoint audience 파라미터
GRAFANA_OAUTH_TOKEN_CACHE 토큰 캐시 경로 override
GRAFANA_OAUTH_AUTH_TIMEOUT device 승인 대기 시간 (기본 10m)

구현 노트

  • AuthRoundTripper 인증 우선순위: OBO > OAuth > service account token > basic auth (OAuth·SA 동시 설정 시 OAuth 우선 + 경고).
  • raw HTTP 도구 클라이언트(BuildTransport)와 OpenAPI 클라이언트(OBO와 동일한 transport-level 주입) 양쪽 적용.
  • 로그인은 lazy + non-blocking: 실제 tool 요청에서만 시작. startup 인증 경로(public-URL fetch, stdio proxied 디스커버리)는 WithoutInteractiveOAuth로 감싸 MCP 핸드셰이크를 막지 않음. device 폴링은 백그라운드 goroutine → tool 호출은 즉시 instructions 반환.
  • access/refresh 토큰은 user config dir(0600)에 캐시, 만료 시 자동 refresh. OAuth 설정은 env에서 1회만 읽어 pointer 공유.
  • golang.org/x/oauth2를 direct dependency로 승격(pkg/browser 의존 제거).

범위 (scope)

핵심 Grafana API + datasource-proxy 계열 도구를 OAuth로 인증합니다. Incident/IRM · OnCall 클라이언트는 여전히 정적 SA 토큰 경로라, 그 도구를 쓰면 SA 토큰이 별도로 필요합니다.

테스트 (Go 1.26, docker)

  • go build ./... ✅ / go vet ✅ / gofmt -l no diff ✅
  • golangci-lint v2.11.4 run ./ ./cmd/... ✅ 0 issues
  • go test -count=1 ./ ./cmd/... ✅ — 기존 인증 테스트 회귀 없음
  • 신규 oauth_test.go: config 파싱, fake IdP 대상 device flow end-to-end(pending→approve→token), 캐시 토큰 silent refresh, non-interactive startup 경로, RoundTripper 우선순위
  • 실 Authentik 검증: tool 호출 시 https://authentik.buzzvil.com/device?code=... + user code를 담은 instructions 즉시 반환(행 없음), 실제 device code 발급 확인

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Grafana 인증에 OAuth2 디바이스 승인 그랜트(RFC 8628)를 신규 도입했습니다. oauth.go에 OAuthConfig 타입, 토큰 캐시/자동 갱신, 인터랙티브 디바이스 로그인 흐름, 컨텍스트 기반 인터랙티브 회피 옵션을 추가했습니다. GrafanaConfig와 AuthRoundTripper가 OAuth를 인식하도록 확장되어 정적 서비스 계정 토큰보다 우선 적용되며, 환경/헤더 기반 설정 추출에서도 OAuth 구성을 로드합니다. main.go의 stdio 초기화 경로는 인터랙티브 로그인을 지연시키도록 컨텍스트를 조정했습니다. go.mod 의존성, README 문서, 관련 테스트가 함께 추가/갱신되었습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Changes

파일 변경 요약
README.md OAuth2 Device Flow 로그인 문서 섹션 추가
cmd/mcp-grafana/main.go stdio 초기화 컨텍스트에 WithoutInteractiveOAuth 적용
go.mod golang.org/x/oauth2를 직접 의존성으로 이동
mcpgrafana.go GrafanaConfig에 OAuth 필드 추가, AuthRoundTripper/설정 추출/클라이언트 생성에 OAuth 통합
oauth.go OAuthConfig, 디바이스 플로우, 토큰 캐시, 환경변수 로딩 신규 구현
oauth_test.go OAuth 관련 단위/통합 테스트 신규 추가

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthRoundTripper
  participant OAuthConfig
  participant IdP
  participant GrafanaAPI

  Client->>AuthRoundTripper: RoundTrip(request)
  AuthRoundTripper->>AuthRoundTripper: access/id 토큰 헤더 설정
  AuthRoundTripper->>OAuthConfig: OAuth.Enabled() 확인
  alt OAuth 활성화
    AuthRoundTripper->>OAuthConfig: Token(allowInteractive)
    OAuthConfig->>OAuthConfig: 캐시/리프레시 확인
    alt 캐시/리프레시 성공
      OAuthConfig-->>AuthRoundTripper: 토큰 반환
    else 인터랙티브 필요
      OAuthConfig->>IdP: startDeviceFlow()
      IdP-->>OAuthConfig: 검증 URL/유저 코드
      OAuthConfig-->>AuthRoundTripper: 안내 에러
    end
    AuthRoundTripper->>AuthRoundTripper: SetAuthHeader(Authorization)
  else OAuth 비활성화
    AuthRoundTripper->>AuthRoundTripper: API key/basic auth 적용
  end
  AuthRoundTripper->>GrafanaAPI: 요청 전송
Loading

Related issues: 명시된 이슈 없음

Related PRs: 명시된 관련 PR 없음

Suggested labels: enhancement, authentication, documentation

Suggested reviewers: 저장소 메인테이너

🐰

토끼가 코드 속을 깡충깡충,
디바이스 코드로 문을 열었죠.
캐시에 토큰 살포시 숨기고,
갱신도 스스로 척척 해내고.
브라우저 없이도 시작은 조용히,
로그인은 나중에, 첫 요청 시에!
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 Grafana용 OAuth2 Device Authorization Grant 로그인의 핵심 변경을 간결하고 정확하게 요약합니다.
Description check ✅ Passed 설명이 OAuth2 디바이스 플로우, 환경변수, 토큰 캐시 및 적용 범위를 실제 변경과 일치하게 잘 설명합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oauth-pkce-sso-login

Comment @coderabbitai help to get the list of available commands.

Add an interactive OAuth2 login so the server can authenticate to Grafana
as the user via an identity provider (SSO) instead of a long-lived service
account token, keeping only short-lived, per-user, revocable tokens on the
machine running the server — no static shared secret.

Uses the Device Authorization Grant (RFC 8628): it has no browser redirect
or loopback callback, so it works even when the server runs somewhere
without a browser (a container, a remote host, over SSH). On the first
Grafana request the server performs a device authorization and returns an
actionable error naming the verification URL and user code; the user
approves in any browser and retries, while the server polls the token
endpoint in the background and then sends the access token as
`Authorization: Bearer`.

- New OAuthConfig (oauth.go) + GRAFANA_OAUTH_CLIENT_ID / _DEVICE_AUTH_URL /
  _TOKEN_URL / _CLIENT_SECRET / _SCOPES / _AUDIENCE / _TOKEN_CACHE /
  _AUTH_TIMEOUT env vars. Public client — no secret required.
- AuthRoundTripper gains OAuth handling, priority
  OBO > OAuth > service account token > basic auth. Applied to raw HTTP
  tool clients (BuildTransport) and the OpenAPI client (transport-level
  injection, like OBO).
- Login is strictly lazy and non-blocking: it only starts on a real tool
  request. Startup work that authenticates to Grafana (the public-URL fetch
  and the stdio proxied-tool discovery) runs under WithoutInteractiveOAuth,
  so it never blocks the MCP handshake; it uses a cached token when present
  and otherwise degrades gracefully. The device poll runs in a background
  goroutine, so the tool call returns immediately with instructions.
- Access/refresh tokens are cached under the user config dir (0600) and
  refreshed automatically. OAuth config is read once from the environment
  and shared by pointer so the cached token is reused across requests.
- Promote golang.org/x/oauth2 to a direct dependency.
- Tests cover config parsing, the full device flow against a fake IdP
  (pending → approve → token), silent refresh, the non-interactive startup
  path, and RoundTripper precedence; README documents the option and scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dlddu
dlddu force-pushed the feat/oauth-pkce-sso-login branch from 76dd754 to cb68377 Compare July 6, 2026 06:22
@dlddu dlddu changed the title feat(auth): OAuth2 Authorization-Code + PKCE login to Grafana feat(auth): OAuth2 Device Authorization Grant login to Grafana Jul 6, 2026
@dlddu
dlddu requested review from a team, C0deWave and hugolee-woobuntu and removed request for a team July 6, 2026 08:56
@dlddu
dlddu marked this pull request as ready for review July 6, 2026 08:56
@C0deWave

C0deWave commented Jul 6, 2026

Copy link
Copy Markdown

요약

이 PR은 정적 서비스 계정 토큰 대신 OAuth2 Device Authorization Grant(RFC 8628)로 Grafana에 로그인하는 기능을 추가합니다. oauth.go에 device flow 시작·폴링·토큰 캐싱/갱신 로직을 구현하고, AuthRoundTripper에 OAuth bearer 토큰 주입을 우선순위(OBO > OAuth > API key > basic)로 통합하며, 서버 시작 시에는 대화형 로그인을 하지 않도록 컨텍스트 플래그를 도입했습니다. 전반적으로 설계가 신중하고 테스트 커버리지도 좋습니다. 다만 멀티유저 HTTP 모드에서의 토큰 공유와 잠금(lock) 하에서의 네트워크 호출 두 가지가 실제 동작에 영향을 줄 수 있습니다.

발견된 이슈

🔴 심각 — HTTP/SSE 멀티유저 모드에서 OAuth 토큰이 프로세스 전역으로 공유됨

mcpgrafana.go:894 (ExtractGrafanaInfoFromHeaders) 에서 config.OAuth = oauthConfigFromEnv(logger) 로 설정하는데, oauthConfigFromEnvsync.Once로 만들어진 단일 싱글턴 *OAuthConfig 를 반환합니다(oauth.go:351-361). 이 포인터는 캐시된 토큰(c.cur)을 내부에 들고 있고, HTTP/SSE 트랜스포트에서는 모든 요청 컨텍스트가 이 동일한 포인터를 공유합니다.

AuthRoundTripper.RoundTrip(mcpgrafana.go:603-623)의 우선순위상 OAuth는 OBO 토큰(X-Access-Token+X-Grafana-Id)이 없을 때 fallback으로 동작합니다. 따라서 여러 사용자가 접속하는 self-hosted HTTP 배포에서, 자체 API 키나 OBO 토큰을 제공하지 않는 요청들은 가장 먼저 device login을 완료한 사용자 A의 Grafana 토큰을 그대로 사용하게 됩니다. 즉 사용자 B가 사용자 A의 Grafana 신원/권한으로 요청하는 교차 사용자 권한 문제가 발생합니다.

  • 이 기능이 stdio(단일 사용자) 전용이라는 것이 설계 의도라면, 최소한 HTTP/SSE 헤더 모드에서는 OAuth fallback을 활성화하지 않거나(예: 문서에 명시된 대로 stdio에서만 적용), 시작 시 경고를 남겨야 합니다. 현재 코드는 헤더 모드에서도 조용히 활성화되므로 위험합니다.
  • README에는 "per-user" 라고 설명되어 있으나 실제 헤더 모드 구현은 per-process 단일 토큰이라 문서와 동작이 어긋납니다.

🟡 주의 — c.mu 잠금을 보유한 채 네트워크 호출을 수행 (동시 요청 전면 블로킹)

OAuthConfig.Token(oauth.go:138-143)은 defer c.mu.Unlock()으로 함수 전체 동안 뮤텍스를 보유합니다. 이 잠금 안에서:

  • c.refresh(c.cur) (oauth.go:157, refreshoauth.go:232-236) — 토큰 갱신을 위해 최대 oauthRefreshTimeout(30초) 동안 블로킹하는 네트워크 호출을 수행합니다.
  • c.startDeviceFlow()(oauth.go:189, 내부 conf.DeviceAuthoauth.go:246) — device authorization POST를 수행하며, 이 컨텍스트의 타임아웃은 authTimeout(기본 10분)입니다.

OAuthConfig는 모든 Grafana 요청이 공유하는 단일 포인터이므로, 토큰 갱신 중이거나 IdP의 device 엔드포인트가 느리게/멈춰서 응답하면 그동안 발생하는 모든 Grafana 요청이 동일한 뮤텍스에서 최대 30초~10분간 직렬화/블로킹됩니다. 갱신/디바이스 요청은 잠금 밖에서 수행하고(예: 이미 존재하는 pending/background goroutine 패턴처럼), 결과만 잠금 안에서 반영하는 구조가 안전합니다. 최소한 초기 DeviceAuth POST에는 authTimeout(10분)보다 훨씬 짧은 별도 타임아웃을 두는 것을 권장합니다.

🟡 주의 — background 폴링 goroutine이 AuthTimeout 동안 살아있고 요청 컨텍스트와 무관

startDeviceFlow(oauth.go:242-277)는 context.Background() 기반 타임아웃 컨텍스트로 goroutine을 띄워 DeviceAccessToken을 폴링합니다. 이는 의도된 설계(백그라운드 로그인)지만, device login이 시작된 뒤 사용자가 승인/취소를 하지 않으면 해당 goroutine은 AuthTimeout(기본 10분) 동안 IdP를 계속 폴링합니다. 짧은 주기로 로그인을 유발하는 요청이 반복되면(예: 매번 실패 후 재시도) goroutine/네트워크 부하가 누적될 수 있습니다. 현재는 pending으로 중복 시작을 막고 있어 실무상 큰 문제는 아니지만, AuthTimeout 값을 크게 설정한 배포에서는 확인이 필요합니다.

의심 코드

Token() 실패한 background 로그인 이후의 재시도 UX (oauth.go:171-186)

background 폴링이 실패(p.err 설정, p.done=true)한 경우, 다음 호출에서 perr를 반환하면서 c.pending을 nil로 클리어합니다. 결과적으로 사용자는 실패 에러를 한 번 받고, 그 다음 재시도에서야 새 device flow가 시작됩니다. 버그는 아니지만, 실패 → 에러 → 재시도 → 새 로그인 지시라는 2단계가 되어 사용자에게 혼란스러울 수 있습니다. 실패 시 곧바로 새 flow를 시작해 지시문을 반환하는 편이 매끄러운지 확인 바랍니다.

토큰 캐시 파일 경로가 클라이언트 설정만으로 결정됨 (oauth.go:299-310)

cachePathClientID|DeviceAuthURL|TokenURL의 sha256으로 파일명을 만듭니다. 동일 OS 사용자 홈 하에서는 문제없지만, 여러 실제 최종 사용자가 동일 설정으로 같은 config 디렉터리를 공유하는 환경(예: 공용 계정)에서는 서로의 토큰을 덮어쓰거나 재사용할 수 있습니다. 0600/0700 권한은 적절합니다. 위 🔴 이슈와 같은 맥락에서 "단일 사용자 전제"가 문서·배포 가이드에 명확한지 확인이 필요합니다.

갱신 응답에 refresh token이 없을 때 (oauth.go:232-236, storeLocked)

golang.org/x/oauth2TokenSource는 갱신 응답에 refresh token이 없으면 기존 것을 보존하므로 정상 동작이 기대됩니다. 다만 일부 IdP가 회전(rotation)된 새 refresh token만 주고 기존 것을 무효화하는 경우, 저장 타이밍/실패 시 refresh token을 잃을 가능성이 있는지 실제 IdP로 확인해 두면 좋겠습니다(테스트는 fake IdP만 커버).

종합 평가

REQUEST_CHANGES

핵심적으로 🔴 항목(HTTP/SSE 헤더 모드에서 프로세스 전역 OAuth 토큰이 여러 사용자 간에 공유되어 교차 사용자 권한 문제가 될 수 있음)에 대한 처리가 필요합니다. 헤더 모드에서 OAuth fallback을 비활성화하거나, 단일 사용자 전제를 코드/문서에서 강제·명시해 주세요. 🟡 잠금 하 네트워크 호출도 다수 동시 요청 환경에서 블로킹을 유발하므로 함께 개선을 권장합니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mcpgrafana.go`:
- Around line 258-265: Update the comments on OAuthConfig and AuthRoundTripper
to match the actual authentication flow implemented in oauth.go: this is OAuth2
Device Authorization Grant (RFC 8628), not Authorization-Code + PKCE or browser
redirect/callback. Keep the wording consistent wherever these symbols are
documented so the comments describe device flow behavior and token acquisition
accurately.
- Around line 892-894: The header-mode fallback currently reuses the
process-wide OAuthConfig from oauthConfigFromEnv(logger), which can leak cached
tokens and login state across requests. Update the header-mode path in
mcpgrafana.go so OAuth is either explicitly disabled there or constructed per
request with isolation, and ensure WithoutInteractiveOAuth is applied
consistently before assigning config.OAuth. Refer to the OAuthConfig setup in
the header-mode handling logic to make the separation clear.

In `@oauth.go`:
- Around line 242-277: `startDeviceFlow()` is holding `c.mu` while calling
`conf.DeviceAuth()`, which blocks other `Token()` calls for the duration of the
IdP request. Move the `DeviceAuth()` call outside the locked section, and keep
`c.mu` only around shared-state updates like `c.pending` and `c.cur` in the
`OAuthConfig` flow. Use the `startDeviceFlow`, `Token`, and `c.mu` symbols to
refactor the lock scope so the slow network call does not serialize unrelated
token requests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bce29b79-4296-4cf1-ad19-3b989800a441

📥 Commits

Reviewing files that changed from the base of the PR and between a1147a7 and cb68377.

📒 Files selected for processing (6)
  • README.md
  • cmd/mcp-grafana/main.go
  • go.mod
  • mcpgrafana.go
  • oauth.go
  • oauth_test.go

Comment thread mcpgrafana.go
Comment on lines +258 to +265
// OAuth holds optional OAuth2 Authorization-Code + PKCE configuration. When
// set and enabled, the server logs the user in interactively via the browser
// and uses the resulting short-lived bearer token to authenticate to Grafana
// instead of a static service account token. It is populated from the
// GRAFANA_OAUTH_* environment variables. It is a pointer so the cached token
// (and its refresh state) is shared across the copies of GrafanaConfig that
// flow through the context.
OAuth *OAuthConfig

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

주석의 인증 방식 설명이 실제 구현과 불일치합니다.

주석은 "OAuth2 Authorization-Code + PKCE ... 브라우저 리다이렉트/콜백"을 전제로 설명하지만, oauth.go의 실제 구현은 리다이렉트/콜백이 없는 OAuth2 Device Authorization Grant(RFC 8628)입니다(oauth.go Line 50-59 참조). Line 569-571의 AuthRoundTripper 주석(“OAuth2 (browser PKCE)”)에도 동일한 불일치가 있습니다. 향후 유지보수 혼란을 줄이도록 device flow로 통일해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcpgrafana.go` around lines 258 - 265, Update the comments on OAuthConfig and
AuthRoundTripper to match the actual authentication flow implemented in
oauth.go: this is OAuth2 Device Authorization Grant (RFC 8628), not
Authorization-Code + PKCE or browser redirect/callback. Keep the wording
consistent wherever these symbols are documented so the comments describe device
flow behavior and token acquisition accurately.

Comment thread mcpgrafana.go
Comment on lines +892 to +894
// OAuth is configured process-wide via GRAFANA_OAUTH_* env vars (not per
// request), so it applies as a fallback the same way in header mode.
config.OAuth = oauthConfigFromEnv(logger)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# OAuth가 HTTP/SSE(헤더) 전송 모드와 함께 사용될 수 있는지, 관련 가드/문서 확인
rg -nP -C3 'oauthConfigFromEnv|WithoutInteractiveOAuth|ExtractGrafanaInfoFromHeaders' --type go
rg -nP -C3 'ComposedHTTPContextFunc|ComposedSSEContextFunc' --type go

Repository: Buzzvil/mcp-grafana

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files | rg '(^|/)(mcpgrafana\.go|oauth\.go|.*grafana.*\.go|.*oauth.*\.go|.*transport.*\.go|.*context.*\.go)$' || true

printf '\n== locate target file ==\n'
fd -a 'mcpgrafana.go' . || true

printf '\n== outline target if found ==\n'
if [ -f mcpgrafana.go ]; then
  ast-grep outline mcpgrafana.go --view expanded || true
fi

printf '\n== search relevant symbols ==\n'
rg -n -C3 'oauthConfigFromEnv|WithoutInteractiveOAuth|ExtractGrafanaInfoFromHeaders|ComposedHTTPContextFunc|ComposedSSEContextFunc|envOAuthOnce|envOAuthConfig|OAuthConfig' . --glob '*.go' || true

Repository: Buzzvil/mcp-grafana

Length of output: 43403


헤더 모드에서 OAuth를 요청별로 분리하세요. mcpgrafana.go:892-894에서 oauthConfigFromEnv(logger)OAuthConfig 포인터와 캐시된 토큰을 프로세스 전역으로 재사용합니다. 이 경로는 HTTP/SSE 헤더 모드에도 그대로 들어가고 WithoutInteractiveOAuth도 적용되지 않으므로, 멀티테넌트 환경에서는 한 요청의 로그인 상태가 다른 요청에 영향을 줄 수 있습니다. 헤더 모드에서 OAuth를 허용할 의도라면 요청별 격리 또는 비활성화 기준을 분리해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcpgrafana.go` around lines 892 - 894, The header-mode fallback currently
reuses the process-wide OAuthConfig from oauthConfigFromEnv(logger), which can
leak cached tokens and login state across requests. Update the header-mode path
in mcpgrafana.go so OAuth is either explicitly disabled there or constructed per
request with isolation, and ensure WithoutInteractiveOAuth is applied
consistently before assigning config.OAuth. Refer to the OAuthConfig setup in
the header-mode handling logic to make the separation clear.

Comment thread oauth.go
Comment on lines +242 to +277
func (c *OAuthConfig) startDeviceFlow() (*deviceFlow, error) {
conf := c.oauth2Config()
ctx, cancel := context.WithTimeout(context.Background(), c.authTimeout())

da, err := conf.DeviceAuth(ctx, c.audienceOptions()...)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to start Grafana OAuth device authorization: %w", err)
}

p := &deviceFlow{
userCode: da.UserCode,
verificationURI: da.VerificationURI,
verificationURIComplete: da.VerificationURIComplete,
}

c.log().Info("Grafana OAuth device login required",
"verification_uri", da.VerificationURI, "user_code", da.UserCode)

go func() {
defer cancel()
tok, err := conf.DeviceAccessToken(ctx, da)

c.mu.Lock()
defer c.mu.Unlock()
if err != nil {
p.err = fmt.Errorf("device login did not complete: %w", err)
} else {
c.storeLocked(tok)
c.log().Info("Grafana OAuth device login completed")
}
p.done = true
}()

return p, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

golang.org/x/oauth2 Config DeviceAuth context timeout behavior

💡 Result:

In the golang.org/x/oauth2 package, the DeviceAuth and DeviceAccessToken methods respect the provided context.Context through the following behaviors: DeviceAuth: This method uses the provided context to perform the initial device authorization request [1][2]. The context is utilized by the internal HTTP client to manage the request's lifecycle, meaning that if the context is canceled or reaches its deadline, the request will be terminated and return an error [1][3]. DeviceAccessToken: This method manages the polling process to exchange a device code for an access token [1][4]. Its behavior regarding the context includes: 1. Lifecycle Management: The method monitors the provided context via a select statement [1][2]. If the context is canceled or times out, the polling loop terminates immediately, and the function returns the context's error [1][3]. 2. Expiry Deadline: If the DeviceAuthResponse contains an Expiry time, the method derives a new context using context.WithDeadline set to that expiry time [1][2]. This ensures that if the device code itself expires, the polling loop will stop even if the user-provided context has a longer timeout or no deadline [1][4]. 3. Polling Intervals: The method respects the RFC 8628 polling interval [1][2]. It specifically handles the authorization_pending and slow_down error codes by either continuing the loop or increasing the polling interval, respectively, all while remaining sensitive to context cancellation at each step of the loop [1][3]. In summary, both methods actively respect the provided context to ensure that requests and polling operations can be cancelled or timed out appropriately [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate oauth.go and print focused regions around Token() and startDeviceFlow()
file=$(git ls-files | rg '(^|/)oauth\.go$' | head -n 1)
echo "FILE=$file"
wc -l "$file"
echo '--- Token() region ---'
sed -n '120,180p' "$file"
echo '--- startDeviceFlow() region ---'
sed -n '220,310p' "$file"

Repository: Buzzvil/mcp-grafana

Length of output: 5338


startDeviceFlow()에서 c.mu를 잡은 채 DeviceAuth()를 호출합니다.
Token()이 이미 이 락을 보유한 상태로 들어오므로, IdP 응답이 지연되면 디바이스 인가 요청이 끝날 때까지 다른 Token() 호출이 모두 대기합니다. 디바이스 인증 요청은 락 밖으로 빼고, c.pending/c.cur 갱신만 락으로 보호하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oauth.go` around lines 242 - 277, `startDeviceFlow()` is holding `c.mu` while
calling `conf.DeviceAuth()`, which blocks other `Token()` calls for the duration
of the IdP request. Move the `DeviceAuth()` call outside the locked section, and
keep `c.mu` only around shared-state updates like `c.pending` and `c.cur` in the
`OAuthConfig` flow. Use the `startDeviceFlow`, `Token`, and `c.mu` symbols to
refactor the lock scope so the slow network call does not serialize unrelated
token requests.

@C0deWave C0deWave left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

더 고트 마제스티 엠페러 사일러스

@C0deWave

C0deWave commented Jul 6, 2026

Copy link
Copy Markdown

( 문제 있으면 그때 수정해도 될꺼라는 판단입니다. )

@hugolee-woobuntu

Copy link
Copy Markdown

요약

이 fork 에는 CI 가 없습니다. head sha cb68377 의 check-run 이 0개이고(gh api repos/Buzzvil/mcp-grafana/commits/cb68377/check-runs), actions/runs 13건이 전부 dependabot dynamic 입니다 — .github/workflows/unit.ymlpull_request 트리거인데 actions/workflows 목록에 등록조차 안 돼 있습니다. 붙은 초록 하나는 CodeRabbit 리뷰 상태이지 빌드·테스트가 아닙니다. 그래서 본문의 테스트 결과를 제가 재현했고 통과했습니다 (go1.26.4 · go build ./... · go vet ./ ./cmd/... · go test -count=1 -run 'OAuth|Auth' ./ → ok).

이미 올라온 두 지적(HTTP 모드 토큰 공유 · 락 하 네트워크 호출)과 겹치지 않는 것만 적습니다.

확인 부탁드리는 것

  • 🟡 oauth.go:307 — 캐시 파일명이 ClientID|DeviceAuthURL|TokenURL 해시라 GRAFANA_OAUTH_AUDIENCEGRAFANA_OAUTH_SCOPES 가 빠져 있습니다. 둘은 DeviceAuth 요청에 실려 발급 토큰을 바꾸는데, 값을 고쳐 재시작해도 oauth.go:151c.cur.Valid() 가 참이면 옛 토큰을 그대로 씁니다. 만료 전까지 설정 변경이 조용히 무시되고, 강제할 수단이 해시 파일명 삭제뿐입니다.
  • 🟡 cmd/mcp-grafana/main.go:292 — stdio 디스커버리는 WithoutInteractiveOAuth 로 감쌌는데(532줄) HTTP/SSE 의 OnBeforeListTools 디스커버리는 raw ctx 입니다. 그러면 핸드셰이크의 tools/list 가 device 로그인을 먼저 열고, 안내 에러는 디스커버리 안에서 삼켜져 사용자에겐 안 보이는 채로 AuthTimeout 만 흐릅니다. 본문의 "stdio proxied 디스커버리"가 의도적 범위 한정인가요?
  • 🟡 mcpgrafana.go:258 · 569 주석이 아직 "Authorization-Code + PKCE" · "browser PKCE" 입니다. 브라우저 리다이렉트가 없다는 게 device flow 를 고른 이유라, 주석만 읽으면 반대로 이해됩니다.

어디까지 봤나

PR head 를 로컬에 받아 빌드·vet·테스트 실행, GitHub API 로 check-run 과 workflow 등록 상태 조회, main.go 의 나머지 디스커버리 경로와 BuildTransportWithoutAuth 호출부 확인. IdP(Authentik) 클라이언트 설정과 실제 device 승인 동작은 안 봤습니다.


Hugo 요청으로 Claude Code 가 조사했습니다. 승인 판단은 Hugo 가 합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants