Support opt-in MFA per browser client with authoritative enrollment checks
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 40s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 40s
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
e1e292919a
commit
ac8ed65203
14 changed files with 298 additions and 12 deletions
|
|
@ -61,6 +61,7 @@ type ClientConfig struct {
|
|||
Tenant string `yaml:"tenant,omitempty"`
|
||||
Roles []string `yaml:"roles,omitempty"`
|
||||
TokenLifetime string `yaml:"tokenLifetime,omitempty"`
|
||||
MFAOptional bool `yaml:"mfaOptional,omitempty"`
|
||||
MFARequired *bool `yaml:"mfaRequired,omitempty"`
|
||||
RegistrationURL string `yaml:"registrationUrl,omitempty"`
|
||||
EnrollmentURL string `yaml:"enrollmentUrl,omitempty"`
|
||||
|
|
@ -130,6 +131,7 @@ func (c *Config) Registrations() ([]domain.Client, error) {
|
|||
Roles: cc.Roles,
|
||||
TokenLifetime: lifetime,
|
||||
MFARequired: cc.MFARequired,
|
||||
MFAOptional: cc.MFAOptional,
|
||||
RegistrationURL: cc.RegistrationURL,
|
||||
EnrollmentURL: cc.EnrollmentURL,
|
||||
})
|
||||
|
|
|
|||
40
src/internal/config/optional_mfa_test.go
Normal file
40
src/internal/config/optional_mfa_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package config_test
|
||||
|
||||
import (
|
||||
"keycape/internal/config"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOptionalMFALoadAndRegistration(t *testing.T) {
|
||||
cfg, err := config.Load(writeTempFile(t, "clients:\n - clientId: demo\n mfaOptional: true\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clients, err := cfg.Registrations()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !clients[0].MFAOptional || clients[0].MFARequired != nil {
|
||||
t.Fatal("optional MFA policy lost in registration")
|
||||
}
|
||||
}
|
||||
func TestOptionalMFAValidation(t *testing.T) {
|
||||
for _, explicit := range []bool{false, true} {
|
||||
cfg := validConfig(writeTempFile(t, "placeholder"))
|
||||
cfg.Clients[0].MFAOptional = true
|
||||
if err := config.ValidateConfig(cfg); len(err) != 0 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg.Clients[0].MFARequired = &explicit
|
||||
if err := config.ValidateConfig(cfg); len(err) == 0 || !strings.Contains(strings.Join(err, ";"), "mfaOptional cannot be combined") {
|
||||
t.Fatalf("ambiguous policy accepted: %v", err)
|
||||
}
|
||||
}
|
||||
cfg := validConfig(writeTempFile(t, "placeholder"))
|
||||
cfg.Clients[0].MFAOptional = true
|
||||
cfg.Clients[0].GrantTypes = []string{"client_credentials"}
|
||||
if err := config.ValidateConfig(cfg); len(err) == 0 || !strings.Contains(strings.Join(err, ";"), "mfaOptional is only supported") {
|
||||
t.Fatalf("service policy accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +78,12 @@ func ValidateConfig(cfg *Config) []string {
|
|||
if strings.TrimSpace(c.Audience) != c.Audience || strings.ContainsAny(c.Audience, " \t\r\n") {
|
||||
errs = append(errs, prefix+": audience must be a single non-whitespace identifier")
|
||||
}
|
||||
if c.MFAOptional && c.MFARequired != nil {
|
||||
errs = append(errs, prefix+": mfaOptional cannot be combined with mfaRequired")
|
||||
}
|
||||
if c.MFAOptional && contains(c.GrantTypes, "client_credentials") {
|
||||
errs = append(errs, prefix+": mfaOptional is only supported for browser clients")
|
||||
}
|
||||
hasAuthorizationCode := contains(c.GrantTypes, "authorization_code")
|
||||
hasClientCredentials := contains(c.GrantTypes, "client_credentials")
|
||||
if (hasAuthorizationCode || !hasClientCredentials) && len(c.RedirectURIs) == 0 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue