generated from coulomb/repo-seed
Require MFA during bootstrap mode
Some checks failed
Build and Publish Container Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Publish Container Image / build-and-push (push) Has been cancelled
This commit is contained in:
@@ -38,6 +38,10 @@ func New(cfg Config, httpClient HTTPClient) *PrivacyIDEAAdapter {
|
||||
// registered in privacyIDEA. Fails closed: any infrastructure error returns
|
||||
// (false, err) so callers cannot bypass the check.
|
||||
func (a *PrivacyIDEAAdapter) CheckMFARequired(ctx context.Context, userID string) (bool, error) {
|
||||
if a.cfg.RequireForAll {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
endpoint := strings.TrimRight(a.cfg.BaseURL, "/") + "/token/"
|
||||
|
||||
q := url.Values{}
|
||||
|
||||
@@ -101,6 +101,27 @@ func TestCheckMFARequired_ActiveTokenPresent_ReturnsTrue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMFARequired_RequireForAll_ReturnsTrueWithoutTokenList(t *testing.T) {
|
||||
client := &mockHTTPClient{
|
||||
doFn: func(_ *http.Request) (*http.Response, error) {
|
||||
t.Fatal("token-list endpoint must not be called when RequireForAll is enabled")
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
cfg := testConfig()
|
||||
cfg.RequireForAll = true
|
||||
adapter := privacyidea.New(cfg, client)
|
||||
|
||||
required, err := adapter.CheckMFARequired(context.Background(), "alice")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !required {
|
||||
t.Error("expected MFA required=true when RequireForAll is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMFARequired_InactiveTokenOnly_ReturnsFalse(t *testing.T) {
|
||||
client := &mockHTTPClient{
|
||||
doFn: func(_ *http.Request) (*http.Response, error) {
|
||||
|
||||
@@ -8,15 +8,20 @@ import "net/http"
|
||||
// Config holds all connection parameters for the privacyIDEA adapter.
|
||||
type Config struct {
|
||||
// BaseURL is the privacyIDEA server base URL, e.g. "https://privacyidea.local".
|
||||
BaseURL string
|
||||
BaseURL string `yaml:"baseURL"`
|
||||
|
||||
// AdminToken is the service-account JWT used to authenticate requests to the
|
||||
// privacyIDEA admin API.
|
||||
AdminToken string
|
||||
AdminToken string `yaml:"adminToken"`
|
||||
|
||||
// Realm is the privacyIDEA realm to scope token and validate requests.
|
||||
// Defaults to "netkingdom" when empty.
|
||||
Realm string
|
||||
Realm string `yaml:"realm"`
|
||||
|
||||
// RequireForAll skips privacyIDEA token-list discovery and requires MFA for
|
||||
// every authenticated upstream user. This is useful during bootstrap when
|
||||
// token-list admin credentials may not be durable yet.
|
||||
RequireForAll bool `yaml:"requireForAll,omitempty"`
|
||||
}
|
||||
|
||||
// realm returns the effective realm, falling back to "netkingdom".
|
||||
|
||||
@@ -127,6 +127,40 @@ clients:
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_PrivacyIDEARequireForAll(t *testing.T) {
|
||||
keyPath := writeTempFile(t, "placeholder-key")
|
||||
yaml := `
|
||||
issuer: "https://kc.example.com"
|
||||
port: 8080
|
||||
tokenLifetime: "15m"
|
||||
privateKeyPem: "` + keyPath + `"
|
||||
environment: "dev"
|
||||
privacyidea:
|
||||
baseURL: "http://privacyidea.mfa.svc.cluster.local:8080"
|
||||
adminToken: "service-token"
|
||||
realm: "coulomb"
|
||||
requireForAll: true
|
||||
clients:
|
||||
- clientId: "netkingdom-bootstrap-console"
|
||||
displayName: "NetKingdom Bootstrap Console"
|
||||
redirectUris:
|
||||
- "http://127.0.0.1:8876/oidc/callback"
|
||||
clientType: "public"
|
||||
`
|
||||
cfgPath := writeTempFile(t, yaml)
|
||||
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: unexpected error: %v", err)
|
||||
}
|
||||
if cfg.PrivacyIDEA.Realm != "coulomb" {
|
||||
t.Errorf("PrivacyIDEA.Realm: got %q", cfg.PrivacyIDEA.Realm)
|
||||
}
|
||||
if !cfg.PrivacyIDEA.RequireForAll {
|
||||
t.Error("PrivacyIDEA.RequireForAll should load from YAML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_FileNotFound(t *testing.T) {
|
||||
_, err := config.Load(filepath.Join(t.TempDir(), "nonexistent.yaml"))
|
||||
if err == nil {
|
||||
|
||||
@@ -279,6 +279,14 @@ func (h *AuthorizeHandler) ServeHTTPCallback(w http.ResponseWriter, r *http.Requ
|
||||
// Check MFA requirement.
|
||||
mfaRequired, err := h.MFA.CheckMFARequired(ctx, result.Username)
|
||||
if err != nil {
|
||||
h.Emitter.Emit(ctx, telemetry.Event{
|
||||
Timestamp: time.Now(),
|
||||
EventType: telemetry.EventAuthFailure,
|
||||
ClientID: ps.ClientID,
|
||||
Endpoint: "/authorize/callback",
|
||||
Result: "failure",
|
||||
ErrorType: "mfa_check_error",
|
||||
})
|
||||
http.Error(w, "mfa check error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user