Skip to content

Add Group-Based Access Control for MCP OAuth Mode#507

Merged
zereight merged 2 commits into
zereight:mainfrom
vrajpal-jhala:feat/group-access-control-oauth
Jun 10, 2026
Merged

Add Group-Based Access Control for MCP OAuth Mode#507
zereight merged 2 commits into
zereight:mainfrom
vrajpal-jhala:feat/group-access-control-oauth

Conversation

@vrajpal-jhala

Copy link
Copy Markdown
Contributor

Overview

This PR adds an optional GITLAB_ALLOWED_GROUPS environment variable that restricts
MCP OAuth server access to users belonging to specific GitLab groups or their
subgroups. When configured, any authenticated user who is not a member of a
matching group receives a 401 Access Denied response.

Changes

Modified Files

  1. config.ts (7 lines added)

    • Added GITLAB_ALLOWED_GROUPS export: parses a comma-separated list of group
      full paths from the env var or --allowed-groups CLI flag
  2. oauth-proxy.ts (38 lines added, 8 lines changed)

    • Added _allowedGroups field and allowedGroups constructor parameter to
      GitLabOAuthServerProvider
    • Added group check in exchangeAuthorizationCode() (both passthrough and
      callback proxy paths) — runs once at token issuance, not on every request
    • Added _checkGroupMembership(): paginates GET /api/v4/groups with the
      user's own bearer token and checks if any returned group's full_path
      equals or is a sub-path of an allowed group
    • Updated createGitLabOAuthProvider() factory signature to accept
      allowedGroups
  3. index.ts (3 lines changed)

    • Added GITLAB_ALLOWED_GROUPS to the config import list
    • Passed GITLAB_ALLOWED_GROUPS through to createGitLabOAuthProvider()

Modified Docs

  1. docs/environment-variables.md (28 lines added)

    • Documented GITLAB_ALLOWED_GROUPS with examples, notes on inheritance
      behaviour, case sensitivity, and security properties
  2. test/mcp-oauth-tests.ts (120 lines added)

    • New describe block: "MCP OAuth — Group Allowlist"
    • 9 unit tests covering: no restriction (default), direct member, first-level
      subgroup, nested subgroup, non-member rejection, prefix-spoofing rejection,
      multiple allowed groups, pagination (match on page 2), case-insensitive
      matching

Implementation Details

Access Check Logic

During token issuance (exchangeAuthorizationCode), when GITLAB_ALLOWED_GROUPS
is set:

  1. Exchange the authorization code with GitLab for an access token (existing step)
  2. Call GET /api/v4/groups?min_access_level=10&per_page=100&page=N using the
    newly issued access token (no extra service account needed)
  3. For each returned group check if full_path equals any allowed group path or
    starts with <allowed>/
  4. Paginate until a match is found or all pages are exhausted
  5. Reject with an error if no match — the client never receives a token

No overhead on subsequent requests: the check does not run on verifyAccessToken.

Inheritance Behaviour

Configured User's group Allowed?
my-org my-org ✅ direct member
my-org my-org/team-a ✅ subgroup member
my-org my-org/team-a/squad-1 ✅ nested subgroup
my-org my-org2 ❌ prefix-spoofing rejected
my-org/team-a my-org ❌ ancestor not granted

Security Properties

  • No squatting: GitLab group full_path values are globally unique on an
    instance — a duplicate path cannot be registered
  • No prefix spoofing: my-org2 does not match my-org (separator / is
    required after the prefix)
  • No server-side secret: the user's own OAuth token is used for the group
    lookup; no additional credentials are stored
  • Immediate enforcement: because verifyAccessToken runs on every request
    in stateful mode, the restriction takes effect the moment the new binary is
    deployed — there is no grace period for existing sessions

Testing & Quality Assurance

TypeScript Compilation: npx tsc --noEmit — no errors
Unit Tests: 9 new test cases on exchangeAuthorizationCode covering all
acceptance, rejection, and edge cases (pagination, case-insensitivity, prefix spoofing)
Backward Compatible: GITLAB_ALLOWED_GROUPS is unset by default; existing
deployments are unaffected

Usage

# Restrict to a single top-level group (and all its subgroups)
GITLAB_ALLOWED_GROUPS=my-company

# Restrict to specific subgroups only
GITLAB_ALLOWED_GROUPS=my-company/engineering,my-company/security

# CLI flag equivalent
--allowed-groups my-company/engineering

Impact Assessment

Breaking Changes

None. The feature is opt-in via GITLAB_ALLOWED_GROUPS. Deployments without
this variable behave exactly as before.

Performance

The group check runs once per OAuth flow (token issuance), not on every request.
In the common case (user in the allowed group, result on page 1) this is one
additional round-trip during login only — zero overhead per tool call.

Checklist

  • TypeScript compilation successful
  • Backward compatibility maintained
  • Unit tests added
  • Documentation updated (docs/environment-variables.md)
  • Security implications reviewed
  • No additional credentials required

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@vrajpal-jhala

Copy link
Copy Markdown
Contributor Author

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. Credits must be used to enable repository wide code reviews.

Hi, @zereight. Have you considered setting up code-rabbit for PR reviews? It's free for OSS projects. Although, I'm not sure about the review quality/accuracy.

@zereight

Copy link
Copy Markdown
Owner

@vrajpal-jhala thanks for comment, i moved to cursor from codex, so codex limit occured.
i knew some days ago, cursor bugbot can not be triggered automatically.
i think code rabbit is good option! i will consider it

@zereight zereight left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Approved

@zereight zereight merged commit dec560b into zereight:main Jun 10, 2026
6 checks passed
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.

2 participants