Accept MCIAS JWT tokens as passwords at token endpoint
The /v2/token endpoint now detects when the password looks like a JWT (contains two dots) and validates it directly against MCIAS before falling back to the standard username+password login flow. This enables non-interactive registry auth for service accounts — podman login with a pre-issued MCIAS token as the password. Follows the personal-access-token pattern used by GHCR, GitLab, etc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,10 +19,19 @@ func (f *fakeLoginClient) Login(_, _ string) (string, int, error) {
|
||||
return f.token, f.expiresIn, f.err
|
||||
}
|
||||
|
||||
type fakeTokenValidator struct {
|
||||
claims *auth.Claims
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeTokenValidator) ValidateToken(_ string) (*auth.Claims, error) {
|
||||
return f.claims, f.err
|
||||
}
|
||||
|
||||
func TestTokenHandlerSuccess(t *testing.T) {
|
||||
t.Helper()
|
||||
lc := &fakeLoginClient{token: "tok-xyz", expiresIn: 7200}
|
||||
handler := TokenHandler(lc)
|
||||
tv := &fakeTokenValidator{err: auth.ErrUnauthorized}
|
||||
handler := TokenHandler(lc, tv)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/token", nil)
|
||||
req.SetBasicAuth("alice", "secret")
|
||||
@@ -49,10 +58,64 @@ func TestTokenHandlerSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHandlerInvalidCreds(t *testing.T) {
|
||||
t.Helper()
|
||||
func TestTokenHandlerJWTAsPassword(t *testing.T) {
|
||||
lc := &fakeLoginClient{err: auth.ErrUnauthorized}
|
||||
handler := TokenHandler(lc)
|
||||
tv := &fakeTokenValidator{claims: &auth.Claims{
|
||||
Subject: "mcp-agent",
|
||||
AccountType: "system",
|
||||
Roles: nil,
|
||||
}}
|
||||
handler := TokenHandler(lc, tv)
|
||||
|
||||
jwt := "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJ0ZXN0In0.c2lnbmF0dXJl"
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/token", nil)
|
||||
req.SetBasicAuth("x", jwt)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var resp tokenResponse
|
||||
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if resp.Token != jwt {
|
||||
t.Fatalf("token: got %q, want JWT pass-through", resp.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHandlerJWTFallsBackToLogin(t *testing.T) {
|
||||
lc := &fakeLoginClient{token: "login-tok", expiresIn: 3600}
|
||||
tv := &fakeTokenValidator{err: auth.ErrUnauthorized}
|
||||
handler := TokenHandler(lc, tv)
|
||||
|
||||
// Password looks like a JWT but validator rejects it — should fall through to login.
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/token", nil)
|
||||
req.SetBasicAuth("alice", "not.a.jwt")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var resp tokenResponse
|
||||
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if resp.Token != "login-tok" {
|
||||
t.Fatalf("token: got %q, want %q (login fallback)", resp.Token, "login-tok")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHandlerInvalidCreds(t *testing.T) {
|
||||
lc := &fakeLoginClient{err: auth.ErrUnauthorized}
|
||||
tv := &fakeTokenValidator{err: auth.ErrUnauthorized}
|
||||
handler := TokenHandler(lc, tv)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/token", nil)
|
||||
req.SetBasicAuth("alice", "wrong")
|
||||
@@ -74,9 +137,9 @@ func TestTokenHandlerInvalidCreds(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTokenHandlerMissingAuth(t *testing.T) {
|
||||
t.Helper()
|
||||
lc := &fakeLoginClient{token: "should-not-matter"}
|
||||
handler := TokenHandler(lc)
|
||||
tv := &fakeTokenValidator{err: auth.ErrUnauthorized}
|
||||
handler := TokenHandler(lc, tv)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/token", nil)
|
||||
// No Authorization header.
|
||||
|
||||
Reference in New Issue
Block a user