Finish KEY-WP-0008: registration handoff and client MFA isolation
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 34s

Add signed registration/enrollment handoffs, per-request assurance
policy with login-session isolation, and /logout. coulomb-social
stays AAL1 unless acr_values or another client raises the bar.
This commit is contained in:
tegwick 2026-08-16 01:05:27 +02:00
parent fff9e39478
commit b6af6c5268
22 changed files with 1636 additions and 42 deletions

View file

@ -41,7 +41,17 @@ func (a *PrivacyIDEAAdapter) CheckMFARequired(ctx context.Context, userID string
if a.cfg.RequireForAll {
return true, nil
}
return a.hasActiveToken(ctx, userID)
}
// HasEnrolledFactor reports whether privacyIDEA has an active token for the
// user. RequireForAll does not skip this check — enrollment is independent
// of the global require-for-all policy.
func (a *PrivacyIDEAAdapter) HasEnrolledFactor(ctx context.Context, userID string) (bool, error) {
return a.hasActiveToken(ctx, userID)
}
func (a *PrivacyIDEAAdapter) hasActiveToken(ctx context.Context, userID string) (bool, error) {
endpoint := strings.TrimRight(a.cfg.BaseURL, "/") + "/token/"
q := url.Values{}

View file

@ -141,6 +141,48 @@ func TestCheckMFARequired_InactiveTokenOnly_ReturnsFalse(t *testing.T) {
}
}
func TestHasEnrolledFactor_RequireForAllStillListsTokens(t *testing.T) {
called := false
client := &mockHTTPClient{
doFn: func(_ *http.Request) (*http.Response, error) {
called = true
return jsonResponse(tokenListResponse(nil)), nil
},
}
cfg := testConfig()
cfg.RequireForAll = true
adapter := privacyidea.New(cfg, client)
enrolled, err := adapter.HasEnrolledFactor(context.Background(), "alice")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if enrolled {
t.Error("expected enrolled=false when no tokens even if RequireForAll")
}
if !called {
t.Error("HasEnrolledFactor must consult the token list")
}
}
func TestHasEnrolledFactor_ActiveToken_ReturnsTrue(t *testing.T) {
client := &mockHTTPClient{
doFn: func(_ *http.Request) (*http.Response, error) {
return jsonResponse(tokenListResponse([]map[string]interface{}{
{"active": true},
})), nil
},
}
adapter := privacyidea.New(testConfig(), client)
enrolled, err := adapter.HasEnrolledFactor(context.Background(), "alice")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !enrolled {
t.Error("expected enrolled=true when an active token is present")
}
}
func TestCheckMFARequired_NoTokens_ReturnsFalse(t *testing.T) {
client := &mockHTTPClient{
doFn: func(_ *http.Request) (*http.Response, error) {