Skip to content

Commit 0e12ae5

Browse files
fix: correct token prefix mapping — EMU uses standard PAT prefixes
ghu_ is OAuth user-to-server (e.g. gh auth login), NOT EMU. EMU users get regular ghp_ (classic) or github_pat_ (fine-grained) tokens — there is no prefix that identifies EMU. EMU is a property of the account, not the token format. Changes: - detect_token_type: ghu_ → 'oauth', gho_ → 'oauth', ghs_/ghr_ → 'github-app' (was all 'classic') - build_error_context: replace token_type=='emu' check with host-based heuristics (*.ghe.com → enterprise msg, github.com → SAML/EMU mention) - Auth docs: remove ghu_ as EMU prefix, clarify EMU uses standard PATs - Agent persona: correct token prefix reference table - Tests: update detect_token_type + build_error_context assertions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 60ff0a5 commit 0e12ae5

5 files changed

Lines changed: 75 additions & 35 deletions

File tree

.github/agents/auth-expert.agent.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ You are an expert on Git hosting authentication across GitHub.com, GitHub Enterp
1313

1414
## Core Knowledge
1515

16-
- **Token types**: Fine-grained PATs (`github_pat_`), classic PATs (`ghp_`), EMU tokens (`ghu_`), OAuth tokens (`gho_`), server tokens (`ghs_`)
17-
- **GitHub EMU constraints**: Enterprise-scoped, cannot access public github.com, `ghu_` prefix
16+
- **Token prefixes**: Fine-grained PATs (`github_pat_`), classic PATs (`ghp_`), OAuth user-to-server (`ghu_` — e.g. `gh auth login`), OAuth app (`gho_`), GitHub App install (`ghs_`), GitHub App refresh (`ghr_`)
17+
- **EMU (Enterprise Managed Users)**: Use standard PAT prefixes (`ghp_`, `github_pat_`). There is NO special prefix for EMU — it's a property of the account, not the token. EMU tokens are enterprise-scoped and cannot access public github.com repos. EMU orgs can exist on github.com or *.ghe.com.
1818
- **Host classification**: github.com (public), *.ghe.com (no public repos), GHES (`GITHUB_HOST`), ADO
1919
- **Git credential helpers**: macOS Keychain, Windows Credential Manager, `gh auth`, `git credential fill`
2020
- **Rate limiting**: 60/hr unauthenticated, 5000/hr authenticated, primary (403) vs secondary (429)
@@ -38,7 +38,7 @@ When reviewing or writing auth code:
3838

3939
## Common Pitfalls
4040

41-
- EMU PATs on public github.com repos → will fail silently
41+
- EMU PATs on public github.com repos → will fail silently (you cannot detect EMU from prefix)
4242
- `git credential fill` only resolves per-host, not per-org
4343
- `_build_repo_url` must accept token param, not use instance var
4444
- Windows: `GIT_ASKPASS` must be `'echo'` not empty string

docs/src/content/docs/getting-started/authentication.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ Per-org tokens take priority over global tokens. Use this when different orgs re
5757

5858
## Enterprise Managed Users (EMU)
5959

60-
EMU orgs can live on **github.com** (e.g., `contoso-microsoft`) or on **GHE Cloud Data Residency** (`*.ghe.com`). EMU tokens (`ghu_` prefix) are enterprise-scoped and cannot access public repos on github.com.
60+
EMU orgs can live on **github.com** (e.g., `contoso-microsoft`) or on **GHE Cloud Data Residency** (`*.ghe.com`). EMU tokens are standard PATs (`ghp_` classic or `github_pat_` fine-grained) — there is no special prefix. They are scoped to the enterprise and cannot access public repos on github.com.
6161

6262
If your manifest mixes enterprise and public packages, use separate tokens:
6363

6464
```bash
65-
export GITHUB_APM_PAT_CONTOSO_MICROSOFT=ghu_emu_token # EMU org (any host)
66-
export GITHUB_APM_PAT=ghp_public_token # public github.com repos
65+
export GITHUB_APM_PAT_CONTOSO_MICROSOFT=github_pat_enterprise_token # EMU org (any host)
66+
export GITHUB_APM_PAT=ghp_public_token # public github.com repos
6767
```
6868

6969
### GHE Cloud Data Residency (`*.ghe.com`)
@@ -128,7 +128,7 @@ Authorize your PAT for SSO at [github.com/settings/tokens](https://github.com/se
128128

129129
### EMU token can't access public repos
130130

131-
EMU tokens (`ghu_` prefix) are enterprise-scoped and cannot access public github.com repos. Use a standard PAT for public repos alongside your EMU token — see [Enterprise Managed Users (EMU)](#enterprise-managed-users-emu) above.
131+
EMU PATs use standard prefixes (`ghp_`, `github_pat_`) — there is no EMU-specific prefix. They are enterprise-scoped and cannot access public github.com repos. Use a standard PAT for public repos alongside your EMU PAT — see [Enterprise Managed Users (EMU)](#enterprise-managed-users-emu) above.
132132

133133
### Diagnosing auth failures
134134

@@ -138,7 +138,7 @@ Run with `--verbose` to see the full resolution chain:
138138
apm install --verbose your-org/package
139139
```
140140

141-
The output shows which env var matched (or `none`), the detected token type (`fine-grained`, `classic`, `emu`), and the host classification (`github`, `ghe_cloud`, `ghes`, `ado`, `generic`).
141+
The output shows which env var matched (or `none`), the detected token type (`fine-grained`, `classic`, `oauth`, `github-app`), and the host classification (`github`, `ghe_cloud`, `ghes`, `ado`, `generic`).
142142

143143
### Git credential helper not found
144144

src/apm_cli/core/auth.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class AuthContext:
6767

6868
token: Optional[str]
6969
source: str # e.g. "GITHUB_APM_PAT_ORGNAME", "GITHUB_TOKEN", "none"
70-
token_type: str # "fine-grained", "classic", "emu", "ado", "artifactory", "unknown"
70+
token_type: str # "fine-grained", "classic", "oauth", "github-app", "unknown"
7171
host_info: HostInfo
7272
git_env: dict = field(compare=False, repr=False)
7373

@@ -142,15 +142,33 @@ def classify_host(host: str) -> HostInfo:
142142

143143
@staticmethod
144144
def detect_token_type(token: str) -> str:
145-
"""Classify a token string by its prefix."""
145+
"""Classify a token string by its prefix.
146+
147+
Note: EMU (Enterprise Managed Users) tokens use standard PAT
148+
prefixes (``ghp_`` or ``github_pat_``). There is no prefix that
149+
identifies a token as EMU-scoped — that's a property of the
150+
account, not the token format.
151+
152+
Prefix reference (docs.github.com):
153+
- ``github_pat_`` → fine-grained PAT
154+
- ``ghp_`` → classic PAT
155+
- ``ghu_`` → OAuth user-to-server (e.g. ``gh auth login``)
156+
- ``gho_`` → OAuth app token
157+
- ``ghs_`` → GitHub App installation (server-to-server)
158+
- ``ghr_`` → GitHub App refresh token
159+
"""
146160
if token.startswith("github_pat_"):
147161
return "fine-grained"
148162
if token.startswith("ghp_"):
149163
return "classic"
150164
if token.startswith("ghu_"):
151-
return "emu"
152-
if token.startswith(("gho_", "ghs_", "ghr_")):
153-
return "classic"
165+
return "oauth"
166+
if token.startswith("gho_"):
167+
return "oauth"
168+
if token.startswith("ghs_"):
169+
return "github-app"
170+
if token.startswith("ghr_"):
171+
return "github-app"
154172
return "unknown"
155173

156174
# -- core resolution ----------------------------------------------------
@@ -264,15 +282,24 @@ def build_error_context(
264282

265283
if auth_ctx.token:
266284
lines.append(f"Token was provided (source: {auth_ctx.source}, type: {auth_ctx.token_type}).")
267-
if auth_ctx.token_type == "emu":
285+
host_info = self.classify_host(host)
286+
if host_info.kind == "ghe_cloud":
268287
lines.append(
269-
"EMU tokens are scoped to your enterprise and cannot "
270-
"access public github.com repos."
288+
"GHE Cloud Data Residency hosts (*.ghe.com) require "
289+
"enterprise-scoped tokens. Ensure your PAT is authorized "
290+
"for this enterprise."
291+
)
292+
elif host.lower() == "github.com":
293+
lines.append(
294+
"If your organization uses SAML SSO or is an EMU org, "
295+
"ensure your PAT is authorized at "
296+
"https://github.com/settings/tokens"
297+
)
298+
else:
299+
lines.append(
300+
"If your organization uses SAML SSO, you may need to "
301+
"authorize your token at https://github.com/settings/tokens"
271302
)
272-
lines.append(
273-
"If your organization uses SAML SSO, you may need to "
274-
"authorize your token at https://github.com/settings/tokens"
275-
)
276303
else:
277304
lines.append("No token available.")
278305
lines.append(

tests/integration/test_auth_resolver.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,13 @@ def test_no_token_suggests_env_vars(self):
286286
assert "GITHUB_APM_PAT" in msg
287287
assert "--verbose" in msg
288288

289-
def test_emu_token_warns(self):
290-
with patch.dict(os.environ, {"GITHUB_APM_PAT": "ghu_emu_abc"}, clear=True), _NO_GIT_CRED:
289+
def test_github_com_error_mentions_emu_sso(self):
290+
"""github.com errors should mention EMU/SSO as possible causes."""
291+
with patch.dict(os.environ, {"GITHUB_APM_PAT": "ghp_some_token"}, clear=True), _NO_GIT_CRED:
291292
resolver = AuthResolver()
292293
msg = resolver.build_error_context("github.com", "clone")
293294

294-
assert "EMU" in msg
295-
assert "enterprise" in msg.lower()
295+
assert "EMU" in msg or "SAML" in msg
296296

297297
def test_org_hint_included(self):
298298
with patch.dict(os.environ, {"GITHUB_APM_PAT": "ghp_tok"}, clear=True), _NO_GIT_CRED:

tests/unit/test_auth.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,17 @@ def test_fine_grained(self):
6060
def test_classic(self):
6161
assert AuthResolver.detect_token_type("ghp_abc123") == "classic"
6262

63-
def test_emu(self):
64-
assert AuthResolver.detect_token_type("ghu_abc123") == "emu"
63+
def test_oauth_user(self):
64+
assert AuthResolver.detect_token_type("ghu_abc123") == "oauth"
6565

66-
def test_oauth(self):
67-
assert AuthResolver.detect_token_type("gho_abc123") == "classic"
66+
def test_oauth_app(self):
67+
assert AuthResolver.detect_token_type("gho_abc123") == "oauth"
6868

69-
def test_server_to_server(self):
70-
assert AuthResolver.detect_token_type("ghs_abc123") == "classic"
69+
def test_github_app_install(self):
70+
assert AuthResolver.detect_token_type("ghs_abc123") == "github-app"
7171

72-
def test_refresh(self):
73-
assert AuthResolver.detect_token_type("ghr_abc123") == "classic"
72+
def test_github_app_refresh(self):
73+
assert AuthResolver.detect_token_type("ghr_abc123") == "github-app"
7474

7575
def test_unknown(self):
7676
assert AuthResolver.detect_token_type("some-random-token") == "unknown"
@@ -328,14 +328,27 @@ def test_no_token_message(self):
328328
assert "GITHUB_APM_PAT" in msg
329329
assert "--verbose" in msg
330330

331-
def test_emu_detection(self):
332-
with patch.dict(os.environ, {"GITHUB_APM_PAT": "ghu_emu_token"}, clear=True):
331+
def test_ghe_cloud_error_context(self):
332+
"""*.ghe.com errors mention enterprise-scoped tokens."""
333+
with patch.dict(os.environ, {"GITHUB_APM_PAT_CONTOSO": "token"}, clear=True):
334+
with patch.object(
335+
GitHubTokenManager, "resolve_credential_from_git", return_value=None
336+
):
337+
resolver = AuthResolver()
338+
msg = resolver.build_error_context(
339+
"contoso.ghe.com", "clone", org="contoso"
340+
)
341+
assert "enterprise" in msg.lower()
342+
343+
def test_github_com_error_mentions_emu(self):
344+
"""github.com errors mention EMU/SSO possibility."""
345+
with patch.dict(os.environ, {"GITHUB_APM_PAT": "ghp_token"}, clear=True):
333346
with patch.object(
334347
GitHubTokenManager, "resolve_credential_from_git", return_value=None
335348
):
336349
resolver = AuthResolver()
337350
msg = resolver.build_error_context("github.com", "clone")
338-
assert "EMU" in msg
351+
assert "EMU" in msg or "SAML" in msg
339352

340353
def test_multi_org_hint(self):
341354
with patch.dict(os.environ, {"GITHUB_APM_PAT": "token"}, clear=True):

0 commit comments

Comments
 (0)