diff --git a/authd-oidc-brokers/conf/variants/oidc/broker.conf b/authd-oidc-brokers/conf/variants/oidc/broker.conf index 8325587984..7191c02c4a 100644 --- a/authd-oidc-brokers/conf/variants/oidc/broker.conf +++ b/authd-oidc-brokers/conf/variants/oidc/broker.conf @@ -24,6 +24,18 @@ client_id = ## if the identity provider is unreachable (e.g. due to network issues). #force_provider_authentication = false +## Disable local password authentication, requiring users to always perform +## device authentication with the identity provider. +## +## When enabled: +## - Users will not be able to create or use local passwords +## - Device authentication will be required for every login +## - Local password authentication mode will not be offered +## +## Important: Enabling this option prevents offline login entirely. +## Users must have network connectivity to authenticate. +#disable_local_password = false + [users] ## The directory where the home directories of new users are created. ## Existing users will keep their current home directory. diff --git a/authd-oidc-brokers/internal/broker/broker.go b/authd-oidc-brokers/internal/broker/broker.go index a46ca2d515..cc6c88d514 100644 --- a/authd-oidc-brokers/internal/broker/broker.go +++ b/authd-oidc-brokers/internal/broker/broker.go @@ -305,6 +305,11 @@ func (b *Broker) availableAuthModes(session session) (availableModes []string, e func (b *Broker) authModeIsAvailable(session session, authMode string) bool { switch authMode { case authmodes.Password: + if b.cfg.disableLocalPassword { + log.Debugf(context.Background(), "Local password authentication is disabled") + return false + } + if !tokenExists(session) { log.Debugf(context.Background(), "Token does not exist for user %q, so local password authentication is not available", session.username) return false @@ -716,9 +721,14 @@ func (b *Broker) deviceAuth(ctx context.Context, session *session) (string, isAu // Store the auth info in the session so that we can use it when handling the // next IsAuthenticated call for the new password mode. session.authInfo = authInfo - session.nextAuthModes = []string{authmodes.NewPassword} - return AuthNext, nil + // Only require password creation if local password authentication is not disabled + if !b.cfg.disableLocalPassword { + session.nextAuthModes = []string{authmodes.NewPassword} + return AuthNext, nil + } + + return b.finishAuth(session, authInfo) } func (b *Broker) passwordAuth(ctx context.Context, session *session, secret string) (string, isAuthenticatedDataResponse) { diff --git a/authd-oidc-brokers/internal/broker/broker_test.go b/authd-oidc-brokers/internal/broker/broker_test.go index 3cc46b7cbe..3d275ccc04 100644 --- a/authd-oidc-brokers/internal/broker/broker_test.go +++ b/authd-oidc-brokers/internal/broker/broker_test.go @@ -537,6 +537,7 @@ func TestIsAuthenticated(t *testing.T) { sessionOffline bool username string forceProviderAuthentication bool + disableLocalPassword bool userDoesNotBecomeOwner bool allUsersAllowed bool extraGroups []string @@ -700,6 +701,14 @@ func TestIsAuthenticated(t *testing.T) { }, address: "127.0.0.1:31315", }, + "Authenticating_with_device_auth_completes_without_newpassword_when_local_password_is_disabled": { + firstSecret: "-", + disableLocalPassword: true, + }, + "Authenticating_with_qrcode_completes_without_newpassword_when_local_password_is_disabled": { + firstSecret: "-", + disableLocalPassword: true, + }, "Error_when_authentication_data_is_invalid": {invalidAuthData: true}, "Error_when_secret_can_not_be_decrypted": {firstMode: authmodes.Password, badFirstKey: true}, @@ -776,6 +785,17 @@ func TestIsAuthenticated(t *testing.T) { token: &tokenOptions{deviceIsDisabled: true}, sessionOffline: true, }, + "Error_when_mode_is_password_and_local_password_is_disabled": { + firstMode: authmodes.Password, + disableLocalPassword: true, + token: &tokenOptions{}, + }, + "Error_when_session_is_for_changing_password_and_local_password_is_disabled": { + sessionMode: sessionmode.ChangePassword, + firstMode: authmodes.Password, + disableLocalPassword: true, + token: &tokenOptions{}, + }, "Error_when_mode_is_invalid": {firstMode: "invalid"}, } for name, tc := range tests { @@ -805,6 +825,7 @@ func TestIsAuthenticated(t *testing.T) { firstUserBecomesOwner: !tc.userDoesNotBecomeOwner, allUsersAllowed: tc.allUsersAllowed, forceProviderAuthentication: tc.forceProviderAuthentication, + disableLocalPassword: tc.disableLocalPassword, extraGroups: tc.extraGroups, ownerExtraGroups: tc.ownerExtraGroups, supportsDeviceRegistration: tc.providerSupportsDeviceRegistration, diff --git a/authd-oidc-brokers/internal/broker/config.go b/authd-oidc-brokers/internal/broker/config.go index 68b7753f45..8832386b3b 100644 --- a/authd-oidc-brokers/internal/broker/config.go +++ b/authd-oidc-brokers/internal/broker/config.go @@ -18,6 +18,8 @@ import ( const ( // forceProviderAuthenticationKey is the key in the config file for the option to force provider authentication during login. forceProviderAuthenticationKey = "force_provider_authentication" + // disableLocalPasswordKey is the key in the config file for the option to disable local password authentication. + disableLocalPasswordKey = "disable_local_password" // oidcSection is the section name in the config file for the OIDC specific configuration. oidcSection = "oidc" @@ -80,6 +82,7 @@ type userConfig struct { issuerURL string forceProviderAuthentication bool + disableLocalPassword bool registerDevice bool allowedUsers map[string]struct{} @@ -234,6 +237,13 @@ func parseConfig(cfgContent []byte, dropInContent []any, p provider) (userConfig return userConfig{}, fmt.Errorf("error parsing '%s': %w", forceProviderAuthenticationKey, err) } } + + if oidc.HasKey(disableLocalPasswordKey) { + cfg.disableLocalPassword, err = oidc.Key(disableLocalPasswordKey).Bool() + if err != nil { + return userConfig{}, fmt.Errorf("error parsing '%s': %w", disableLocalPasswordKey, err) + } + } } entraID := iniCfg.Section(entraIDSection) diff --git a/authd-oidc-brokers/internal/broker/config_test.go b/authd-oidc-brokers/internal/broker/config_test.go index 15d3538d19..a589a3c2db 100644 --- a/authd-oidc-brokers/internal/broker/config_test.go +++ b/authd-oidc-brokers/internal/broker/config_test.go @@ -28,17 +28,25 @@ issuer = https://issuer.url.com client_id = client_id force_provider_authentication = true extra_scopes = groups,offline_access, some_other_scope +disable_local_password = true [users] home_base_dir = /home allowed_ssh_suffixes = @issuer.url.com `, - "invalid_boolean_value": ` + "invalid_force_provider_authentication_boolean_value": ` [oidc] issuer = https://issuer.url.com client_id = client_id force_provider_authentication = invalid +`, + + "invalid_disable_local_password_boolean_value": ` +[oidc] +issuer = https://issuer.url.com +client_id = client_id +disable_local_password = invalid `, "singles": ` @@ -82,12 +90,13 @@ func TestParseConfig(t *testing.T) { "Do_not_fail_if_values_contain_a_single_template_delimiter": {configType: "singles"}, - "Error_if_file_does_not_exist": {configType: "inexistent", wantErr: true}, - "Error_if_file_is_unreadable": {configType: "unreadable", wantErr: true}, - "Error_if_file_is_not_updated": {configType: "template", wantErr: true}, - "Error_if_drop_in_directory_is_unreadable": {dropInType: "unreadable-dir", wantErr: true}, - "Error_if_drop_in_file_is_unreadable": {dropInType: "unreadable-file", wantErr: true}, - "Error_if_config_contains_invalid_values": {configType: "invalid_boolean_value", wantErr: true}, + "Error_if_file_does_not_exist": {configType: "inexistent", wantErr: true}, + "Error_if_file_is_unreadable": {configType: "unreadable", wantErr: true}, + "Error_if_file_is_not_updated": {configType: "template", wantErr: true}, + "Error_if_drop_in_directory_is_unreadable": {dropInType: "unreadable-dir", wantErr: true}, + "Error_if_drop_in_file_is_unreadable": {dropInType: "unreadable-file", wantErr: true}, + "Error_if_force_provider_authentication_contains_invalid_boolean_value": {configType: "invalid_force_provider_authentication_boolean_value", wantErr: true}, + "Error_if_disable_local_password_contains_invalid_boolean_value": {configType: "invalid_disable_local_password_boolean_value", wantErr: true}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { diff --git a/authd-oidc-brokers/internal/broker/export_test.go b/authd-oidc-brokers/internal/broker/export_test.go index 816d8209c3..c10536a250 100644 --- a/authd-oidc-brokers/internal/broker/export_test.go +++ b/authd-oidc-brokers/internal/broker/export_test.go @@ -20,6 +20,10 @@ func (cfg *Config) SetForceProviderAuthentication(value bool) { cfg.forceProviderAuthentication = value } +func (cfg *Config) SetDisableLocalPassword(value bool) { + cfg.disableLocalPassword = value +} + func (cfg *Config) SetRegisterDevice(value bool) { cfg.registerDevice = value } diff --git a/authd-oidc-brokers/internal/broker/helper_test.go b/authd-oidc-brokers/internal/broker/helper_test.go index eaf23aa0c1..d37eea4654 100644 --- a/authd-oidc-brokers/internal/broker/helper_test.go +++ b/authd-oidc-brokers/internal/broker/helper_test.go @@ -26,6 +26,7 @@ type brokerForTestConfig struct { broker.Config issuerURL string forceProviderAuthentication bool + disableLocalPassword bool registerDevice bool allowedUsers map[string]struct{} allUsersAllowed bool @@ -60,6 +61,9 @@ func newBrokerForTests(t *testing.T, cfg *brokerForTestConfig) (b *broker.Broker if cfg.forceProviderAuthentication { cfg.SetForceProviderAuthentication(cfg.forceProviderAuthentication) } + if cfg.disableLocalPassword { + cfg.SetDisableLocalPassword(cfg.disableLocalPassword) + } if cfg.registerDevice { cfg.SetRegisterDevice(cfg.registerDevice) } diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_device_auth_completes_without_newpassword_when_local_password_is_disabled/data/provider_url/test-user@email.com/token.json b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_device_auth_completes_without_newpassword_when_local_password_is_disabled/data/provider_url/test-user@email.com/token.json new file mode 100644 index 0000000000..ecaed7cd75 --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_device_auth_completes_without_newpassword_when_local_password_is_disabled/data/provider_url/test-user@email.com/token.json @@ -0,0 +1 @@ +Definitely a token \ No newline at end of file diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_device_auth_completes_without_newpassword_when_local_password_is_disabled/first_call b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_device_auth_completes_without_newpassword_when_local_password_is_disabled/first_call new file mode 100644 index 0000000000..f50b5eb551 --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_device_auth_completes_without_newpassword_when_local_password_is_disabled/first_call @@ -0,0 +1,3 @@ +access: granted +data: '{"userinfo":{"name":"test-user@email.com","uuid":"test-user-id","dir":"/home/test-user@email.com","shell":"/usr/bin/bash","gecos":"test-user@email.com","groups":[{"name":"remote-test-group","ugid":"12345"},{"name":"local-test-group","ugid":""}]}}' +err: diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_qrcode_completes_without_newpassword_when_local_password_is_disabled/data/provider_url/test-user@email.com/token.json b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_qrcode_completes_without_newpassword_when_local_password_is_disabled/data/provider_url/test-user@email.com/token.json new file mode 100644 index 0000000000..ecaed7cd75 --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_qrcode_completes_without_newpassword_when_local_password_is_disabled/data/provider_url/test-user@email.com/token.json @@ -0,0 +1 @@ +Definitely a token \ No newline at end of file diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_qrcode_completes_without_newpassword_when_local_password_is_disabled/first_call b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_qrcode_completes_without_newpassword_when_local_password_is_disabled/first_call new file mode 100644 index 0000000000..f50b5eb551 --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Authenticating_with_qrcode_completes_without_newpassword_when_local_password_is_disabled/first_call @@ -0,0 +1,3 @@ +access: granted +data: '{"userinfo":{"name":"test-user@email.com","uuid":"test-user-id","dir":"/home/test-user@email.com","shell":"/usr/bin/bash","gecos":"test-user@email.com","groups":[{"name":"remote-test-group","ugid":"12345"},{"name":"local-test-group","ugid":""}]}}' +err: diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/password b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/password new file mode 100644 index 0000000000..119947240f --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/password @@ -0,0 +1 @@ +Definitely a hashed password \ No newline at end of file diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/token.json b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/token.json new file mode 100644 index 0000000000..ecaed7cd75 --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/token.json @@ -0,0 +1 @@ +Definitely a token \ No newline at end of file diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/first_call b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/first_call new file mode 100644 index 0000000000..f50b5eb551 --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_mode_is_password_and_local_password_is_disabled/first_call @@ -0,0 +1,3 @@ +access: granted +data: '{"userinfo":{"name":"test-user@email.com","uuid":"test-user-id","dir":"/home/test-user@email.com","shell":"/usr/bin/bash","gecos":"test-user@email.com","groups":[{"name":"remote-test-group","ugid":"12345"},{"name":"local-test-group","ugid":""}]}}' +err: diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/password b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/password new file mode 100644 index 0000000000..119947240f --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/password @@ -0,0 +1 @@ +Definitely a hashed password \ No newline at end of file diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/token.json b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/token.json new file mode 100644 index 0000000000..ecaed7cd75 --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/data/provider_url/test-user@email.com/token.json @@ -0,0 +1 @@ +Definitely a token \ No newline at end of file diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/first_call b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/first_call new file mode 100644 index 0000000000..d0887a134f --- /dev/null +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestIsAuthenticated/Error_when_session_is_for_changing_password_and_local_password_is_disabled/first_call @@ -0,0 +1,3 @@ +access: next +data: '{}' +err: diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Do_not_fail_if_values_contain_a_single_template_delimiter/config.txt b/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Do_not_fail_if_values_contain_a_single_template_delimiter/config.txt index c68cb5a100..5fd94ac90b 100644 --- a/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Do_not_fail_if_values_contain_a_single_template_delimiter/config.txt +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Do_not_fail_if_values_contain_a_single_template_delimiter/config.txt @@ -2,6 +2,7 @@ clientID= forceProviderAuthentication=false +disableLocalPassword=false registerDevice=false allowedUsers=map[] allUsersAllowed=false diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_file/config.txt b/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_file/config.txt index ce860f6ee7..a152d8ceb1 100644 --- a/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_file/config.txt +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_file/config.txt @@ -2,6 +2,7 @@ clientID=client_id clientSecret= issuerURL=https://issuer.url.com forceProviderAuthentication=false +disableLocalPassword=false registerDevice=false allowedUsers=map[] allUsersAllowed=false diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_file_with_optional_values/config.txt b/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_file_with_optional_values/config.txt index 186c12ac12..2065776ca0 100644 --- a/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_file_with_optional_values/config.txt +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_file_with_optional_values/config.txt @@ -2,6 +2,7 @@ clientID=client_id clientSecret= issuerURL=https://issuer.url.com forceProviderAuthentication=true +disableLocalPassword=true registerDevice=false allowedUsers=map[] allUsersAllowed=false diff --git a/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_with_drop_in_files/config.txt b/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_with_drop_in_files/config.txt index 984aee7a70..bd1fe93b70 100644 --- a/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_with_drop_in_files/config.txt +++ b/authd-oidc-brokers/internal/broker/testdata/golden/TestParseConfig/Successfully_parse_config_with_drop_in_files/config.txt @@ -2,6 +2,7 @@ clientID=lower_precedence_client_id clientSecret= issuerURL=https://higher-precedence-issuer.url.com forceProviderAuthentication=true +disableLocalPassword=true registerDevice=false allowedUsers=map[] allUsersAllowed=false