Implement KeyCape service-token issuance
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 47s

This commit is contained in:
tegwick 2026-07-27 20:03:07 +02:00
parent 519f0772d2
commit e877d2752d
10 changed files with 348 additions and 43 deletions

View file

@ -209,7 +209,7 @@ func TestTokenHandler_WrongGrantType_FeatureNotSupported(t *testing.T) {
h, _ := newTokenHandler(t, sessions, users)
params := url.Values{
"grant_type": []string{"client_credentials"},
"grant_type": []string{"password"},
"client_id": []string{"test-client"},
}
@ -226,6 +226,72 @@ func TestTokenHandler_WrongGrantType_FeatureNotSupported(t *testing.T) {
}
}
func serviceTokenHandler(t *testing.T) *oidc.TokenHandler {
t.Helper()
h, _ := newTokenHandler(t, oidc.NewSessionStore(), &mockUserRepo{})
h.ClientConfig["rapp-qonto"] = &domain.Client{
ClientID: "rapp-qonto",
AllowedScopes: []string{"finance.qonto.read"},
GrantTypes: []string{"client_credentials"},
ClientType: "confidential",
ClientSecret: "test-service-secret",
ServiceSubject: "service:rapp-qonto",
Tenant: "tenant:friendly:binky",
Roles: []string{"finance-reader"},
}
return h
}
func TestTokenHandler_ClientCredentials_ReturnsScopedServiceToken(t *testing.T) {
h := serviceTokenHandler(t)
req := tokenRequest(url.Values{
"grant_type": {"client_credentials"},
"scope": {"finance.qonto.read"},
})
req.SetBasicAuth("rapp-qonto", "test-service-secret")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
resp := decodeTokenResponse(t, w.Body.String())
if _, ok := resp["id_token"]; ok {
t.Fatal("service exchange must not return id_token")
}
claims := parseJWTPayload(t, resp["access_token"].(string))
if claims["sub"] != "service:rapp-qonto" ||
claims["tenant"] != "tenant:friendly:binky" ||
claims["principal_type"] != "service" ||
claims["scope"] != "finance.qonto.read" {
t.Fatalf("unexpected service claims: %#v", claims)
}
}
func TestTokenHandler_ClientCredentials_RejectsWrongSecret(t *testing.T) {
h := serviceTokenHandler(t)
req := tokenRequest(url.Values{"grant_type": {"client_credentials"}})
req.SetBasicAuth("rapp-qonto", "wrong")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", w.Code)
}
}
func TestTokenHandler_ClientCredentials_RejectsExcessScope(t *testing.T) {
h := serviceTokenHandler(t)
req := tokenRequest(url.Values{
"grant_type": {"client_credentials"},
"scope": {"finance.qonto.write"},
})
req.SetBasicAuth("rapp-qonto", "test-service-secret")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", w.Code)
}
}
func TestTokenHandler_PKCEMismatch_InvalidProfileUsage(t *testing.T) {
sessions := oidc.NewSessionStore()
users := &mockUserRepo{users: map[string]*domain.User{"alice": aliceUser()}}