diff --git a/examplebroker/broker.go b/examplebroker/broker.go index fe7ec4e6d..7623d9098 100644 --- a/examplebroker/broker.go +++ b/examplebroker/broker.go @@ -169,6 +169,12 @@ func (b *Broker) NewSession(ctx context.Context, username, lang, mode string) (s info.neededAuthSteps = 3 } + if _, ok := exampleUsers[username]; !ok && strings.HasPrefix(username, "user-needs-reset-integration") { + exampleUsers[username] = userInfoBroker{Password: "goodpass"} + info.neededAuthSteps = 2 + info.pwdChange = mustReset + } + pubASN1, err := x509.MarshalPKIXPublicKey(&b.privateKey.PublicKey) if err != nil { return "", "", err @@ -623,8 +629,15 @@ func (b *Broker) handleIsAuthenticated(ctx context.Context, sessionInfo sessionI exampleUsersMu.Lock() defer exampleUsersMu.Unlock() - if challenge != "authd2404" { - return AuthRetry, `{"message": "new password does not match criteria: must be authd2404"}`, nil + expectedChallenge := "authd2404" + // Reset the password to default if it had already been changed. + // As at PAM level we'd refuse a previous password to be re-used. + if exampleUsers[sessionInfo.username].Password == expectedChallenge { + expectedChallenge = "goodpass" + } + + if challenge != expectedChallenge { + return AuthRetry, fmt.Sprintf(`{"message": "new password does not match criteria: must be '%s'"}`, expectedChallenge), nil } exampleUsers[sessionInfo.username] = userInfoBroker{Password: challenge} } diff --git a/pam/integration-tests/gdm_test.go b/pam/integration-tests/gdm_test.go index b8ed2042b..c322c6b44 100644 --- a/pam/integration-tests/gdm_test.go +++ b/pam/integration-tests/gdm_test.go @@ -34,10 +34,11 @@ const ( exampleBrokerName = "ExampleBroker" ignoredBrokerName = "" - passwordAuthID = "password" - fido1AuthID = "fidodevice1" - phoneAck1ID = "phoneack1" - qrcodeID = "qrcodewithtypo" + passwordAuthID = "password" + newPasswordAuthID = "mandatoryreset" + fido1AuthID = "fidodevice1" + phoneAck1ID = "phoneack1" + qrcodeID = "qrcodewithtypo" ) var testPasswordUILayout = authd.UILayout{ @@ -50,6 +51,16 @@ var testPasswordUILayout = authd.UILayout{ Wait: ptrValue(""), } +var testNewPasswordUILayout = authd.UILayout{ + Type: "newpassword", + Label: ptrValue("Enter your new password"), + Entry: ptrValue("chars_password"), + Button: ptrValue(""), + Code: ptrValue(""), + Content: ptrValue(""), + Wait: ptrValue(""), +} + var testQrcodeUILayout = authd.UILayout{ Type: "qrcode", Label: ptrValue("Enter the following code after flashing the address: 1337"), @@ -166,6 +177,63 @@ func TestGdmModule(t *testing.T) { }, }, }, + "Authenticates after password change": { + pamUser: ptrValue("user-needs-reset-integration-gdm-pass"), + wantAuthModeIDs: []string{passwordAuthID, newPasswordAuthID}, + supportedLayouts: []*authd.UILayout{ + pam_test.FormUILayout(), + pam_test.NewPasswordUILayout(), + }, + eventPollResponses: map[gdm.EventType][]*gdm.EventData{ + gdm.EventType_startAuthentication: { + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "goodpass", + }), + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "authd2404", + }), + }, + }, + wantUILayouts: []*authd.UILayout{&testPasswordUILayout, &testNewPasswordUILayout}, + }, + "Authenticates after various invalid password changes": { + pamUser: ptrValue("user-needs-reset-integration-gdm-retries"), + wantAuthModeIDs: []string{ + passwordAuthID, + newPasswordAuthID, + newPasswordAuthID, + newPasswordAuthID, + newPasswordAuthID, + newPasswordAuthID, + }, + supportedLayouts: []*authd.UILayout{ + pam_test.FormUILayout(), + pam_test.NewPasswordUILayout(), + }, + eventPollResponses: map[gdm.EventType][]*gdm.EventData{ + gdm.EventType_startAuthentication: { + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "goodpass", + }), + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "authd", + }), + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "goodpass", + }), + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "foolinux", + }), + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "newpass", + }), + gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "authd2404", + }), + }, + }, + wantUILayouts: []*authd.UILayout{&testPasswordUILayout, &testNewPasswordUILayout}, + }, "Authenticates user with qrcode": { wantAuthModeIDs: []string{qrcodeID}, supportedLayouts: []*authd.UILayout{pam_test.QrCodeUILayout()}, diff --git a/pam/integration-tests/testdata/TestCLIChangeAuthTok/golden/retry_if_new_password_is_rejected_by_broker b/pam/integration-tests/testdata/TestCLIChangeAuthTok/golden/retry_if_new_password_is_rejected_by_broker index cf835cb4b..481432a3c 100644 --- a/pam/integration-tests/testdata/TestCLIChangeAuthTok/golden/retry_if_new_password_is_rejected_by_broker +++ b/pam/integration-tests/testdata/TestCLIChangeAuthTok/golden/retry_if_new_password_is_rejected_by_broker @@ -234,7 +234,7 @@ Enter your new password New password: > -new password does not match criteria: must be authd2404 +new password does not match criteria: must be 'authd2404' @@ -267,7 +267,7 @@ Enter your new password New password: > ********* -new password does not match criteria: must be authd2404 +new password does not match criteria: must be 'authd2404' @@ -390,6 +390,369 @@ PAM AcctMgmt() exited with success + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Username: user name + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Username: user-integration-invalid-new-password + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Gimme your password +> + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +> ********* + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> +new password does not match criteria: must be 'goodpass' + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ******** +new password does not match criteria: must be 'goodpass' + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ******** +Confirm password: +> ******** + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ******** +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ********* +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} +Enter your new password + +New password: +> ******** +Confirm password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> + + + + + + + + + + + + diff --git a/pam/integration-tests/testdata/TestNativeChangeAuthTok/golden/retry_if_new_password_is_rejected_by_broker b/pam/integration-tests/testdata/TestNativeChangeAuthTok/golden/retry_if_new_password_is_rejected_by_broker index e749f2329..4b87c6840 100644 --- a/pam/integration-tests/testdata/TestNativeChangeAuthTok/golden/retry_if_new_password_is_rejected_by_broker +++ b/pam/integration-tests/testdata/TestNativeChangeAuthTok/golden/retry_if_new_password_is_rejected_by_broker @@ -20,6 +20,255 @@ Username: + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: + + + + + + + + + + + + + + + + + + + + + + + + @@ -33,11 +282,213 @@ Username: ──────────────────────────────────────────────────────────────────────────────── > ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> @@ -69,8 +520,22 @@ Username: user-integration-invalid-new-password == Broker selection (use 'r' to go back) == 1 - local 2 - ExampleBroker -Select broker: - +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> @@ -105,6 +570,20 @@ Username: user-integration-invalid-new-password Select broker: 2 Insert 'r' to cancel the request and go back Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: @@ -140,6 +619,20 @@ Insert 'r' to cancel the request and go back Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password + + @@ -173,9 +666,23 @@ Insert 'r' to cancel the request and go back Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' Insert 'r' to cancel the request and go back Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +Insert 'r' to cancel the request and go back +Gimme your password: + + + @@ -206,7 +713,20 @@ Insert 'r' to cancel the request and go back Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +Insert 'r' to cancel the request and go back +Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: @@ -228,6 +748,7 @@ Enter your new password: + ──────────────────────────────────────────────────────────────────────────────── > ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true Username: user-integration-invalid-new-password @@ -239,12 +760,26 @@ Insert 'r' to cancel the request and go back Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +Insert 'r' to cancel the request and go back +Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: + + @@ -272,10 +807,20 @@ Insert 'r' to cancel the request and go back Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +Insert 'r' to cancel the request and go back +Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: Repeat the previously inserted password or insert 'r' to cancel the request and go back @@ -294,6 +839,10 @@ Enter your new password: + + + + ──────────────────────────────────────────────────────────────────────────────── > ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true Username: user-integration-invalid-new-password @@ -305,10 +854,9 @@ Insert 'r' to cancel the request and go back Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters -Insert 'r' to cancel the request and go back +Repeat the previously inserted password or insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +new password does not match criteria: must be 'authd2404' Insert 'r' to cancel the request and go back Enter your new password: Repeat the previously inserted password or insert 'r' to cancel the request and go back @@ -316,6 +864,21 @@ Enter your new password: PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success PAM AcctMgmt() exited with success > +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'goodpass' +Insert 'r' to cancel the request and go back +Enter your new password: + + + + @@ -338,10 +901,56 @@ Insert 'r' to cancel the request and go back Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'goodpass' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' Insert 'r' to cancel the request and go back Enter your new password: Repeat the previously inserted password or insert 'r' to cancel the request and go back @@ -349,6 +958,21 @@ Enter your new password: PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success PAM AcctMgmt() exited with success > +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'goodpass' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success > @@ -371,10 +995,25 @@ Insert 'r' to cancel the request and go back Gimme your password: Insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back Enter your new password: -The password is shorter than 8 characters +new password does not match criteria: must be 'goodpass' Insert 'r' to cancel the request and go back Enter your new password: Repeat the previously inserted password or insert 'r' to cancel the request and go back @@ -392,5 +1031,51 @@ PAM AcctMgmt() exited with success +──────────────────────────────────────────────────────────────────────────────── +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +== Broker selection (use 'r' to go back) == +1 - local +2 - ExampleBroker +Select broker: 2 +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'authd2404' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> ./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true +Username: user-integration-invalid-new-password +Insert 'r' to cancel the request and go back +Gimme your password: +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +new password does not match criteria: must be 'goodpass' +Insert 'r' to cancel the request and go back +Enter your new password: +Repeat the previously inserted password or insert 'r' to cancel the request and go back +Enter your new password: +PAM ChangeAuthTok() for user "user-integration-invalid-new-password" exited with success +PAM AcctMgmt() exited with success +> +> + + + + + + + + ──────────────────────────────────────────────────────────────────────────────── diff --git a/pam/integration-tests/testdata/tapes/cli/passwd_rejected.tape b/pam/integration-tests/testdata/tapes/cli/passwd_rejected.tape index 64f2917a7..f9bdf01a6 100644 --- a/pam/integration-tests/testdata/tapes/cli/passwd_rejected.tape +++ b/pam/integration-tests/testdata/tapes/cli/passwd_rejected.tape @@ -74,4 +74,64 @@ Enter Sleep 2s Show +Sleep 1s + +# Repeat again, to check that we can use still use another new password + +Hide +Type "./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK}" +Enter +Sleep 300ms +Show + +Hide +Escape +Backspace +Type "user-integration-invalid-new-password" +Sleep 300ms +Show + +Hide +Enter +Sleep 300ms +Show + +Hide +Type "authd2404" +Enter +Sleep 2s +Show + +Hide +Type "noble2404" +Sleep 300ms +Show + +Hide +Enter +Type "noble2404" +Sleep 300ms +Show + +Hide +Enter +Sleep 2s +Show + +Hide +Type "goodpass" +Sleep 300ms +Show + +Hide +Enter +Type "goodpass" +Sleep 300ms +Show + +Hide +Enter +Sleep 2s +Show + Sleep 300ms diff --git a/pam/integration-tests/testdata/tapes/native/passwd_rejected.tape b/pam/integration-tests/testdata/tapes/native/passwd_rejected.tape index 364c3b38e..b9bb1f2b8 100644 --- a/pam/integration-tests/testdata/tapes/native/passwd_rejected.tape +++ b/pam/integration-tests/testdata/tapes/native/passwd_rejected.tape @@ -4,7 +4,9 @@ Output passwd_rejected.gif # If we don't specify a .gif output, it will create a # Configuration header to standardize the output. # Does not work with the "Source" command. Set Width 800 -Set Height 500 +# We need high terminal view, as vhs doesn't scroll: +# https://github.com/charmbracelet/vhs/issues/404 +Set Height 700 # TODO: Ideally, we should use Ubuntu Mono. However, the github runner is still on Jammy, which does not have it. # We should update this to use Ubuntu Mono once the runner is updated. Set FontFamily "Monospace" @@ -42,13 +44,13 @@ Sleep 2s Show Hide -Type "badpass" +Type "noble2404" Enter Sleep 300ms Show Hide -Type "badpass" +Type "noble2404" Sleep 300ms Show @@ -74,4 +76,63 @@ Enter Sleep 2s Show +# Repeat again, to check that we can use still use another new password + +Sleep 1s + +Hide +Type "./pam_authd passwd socket=${AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK} force_native_client=true" +Enter +Sleep 300ms +Show + +Hide +Type "user-integration-invalid-new-password" +Sleep 300ms +Show + +Hide +Enter +Sleep 300ms +Show + +Hide +Type "authd2404" +Enter +Sleep 2s +Show + +Hide +Type "noble2404" +Enter +Sleep 300ms +Show + +Hide +Type "noble2404" +Sleep 300ms +Show + +Hide +Enter +Sleep 2s +Show + +Hide +Type "goodpass" +Enter +Sleep 300ms +Show + +Hide +Type "goodpass" +Enter +Sleep 300ms +Show + +Hide +Enter +Sleep 2s +Show + Sleep 300ms diff --git a/pam/internal/adapter/authentication.go b/pam/internal/adapter/authentication.go index 926c37122..3b1bc8370 100644 --- a/pam/internal/adapter/authentication.go +++ b/pam/internal/adapter/authentication.go @@ -36,7 +36,7 @@ var ( // sendIsAuthenticated sends the authentication challenges or wait request to the brokers. // The event will contain the returned value from the broker. func sendIsAuthenticated(ctx context.Context, client authd.PAMClient, sessionID string, - authData *authd.IARequest_AuthenticationData) tea.Cmd { + authData *authd.IARequest_AuthenticationData, challenge string) tea.Cmd { return func() tea.Msg { res, err := client.IsAuthenticated(ctx, &authd.IARequest{ SessionId: sessionID, @@ -53,7 +53,8 @@ func sendIsAuthenticated(ctx context.Context, client authd.PAMClient, sessionID <-time.After(cancellationWait * 3) return isAuthenticatedResultReceived{ - access: brokers.AuthCancelled, + access: brokers.AuthCancelled, + challenge: challenge, } } return pamError{ @@ -63,8 +64,9 @@ func sendIsAuthenticated(ctx context.Context, client authd.PAMClient, sessionID } return isAuthenticatedResultReceived{ - access: res.Access, - msg: res.Msg, + access: res.Access, + msg: res.Msg, + challenge: challenge, } } } @@ -75,6 +77,8 @@ type isAuthenticatedRequested struct { item authd.IARequestAuthenticationDataItem } +// isAuthenticatedRequestedSend is the internal event signaling that the authentication +// request should be sent to the broker. type isAuthenticatedRequestedSend struct { isAuthenticatedRequested ctx context.Context @@ -83,8 +87,9 @@ type isAuthenticatedRequestedSend struct { // isAuthenticatedResultReceived is the internal event with the authentication access result // and data that was retrieved. type isAuthenticatedResultReceived struct { - access string - msg string + access string + challenge string + msg string } // isAuthenticatedCancelled is the event to cancel the auth request. @@ -116,6 +121,7 @@ type authenticationModel struct { currentSessionID string currentBrokerID string currentChallenge string + currentLayout string cancelAuthFunc func() authTracker *authTracker @@ -141,11 +147,13 @@ type errMsgToDisplay struct { // newPasswordCheck is sent to request a new password quality check. type newPasswordCheck struct { + ctx context.Context challenge string } // newPasswordCheckResult returns the password quality check result. type newPasswordCheckResult struct { + ctx context.Context challenge string msg string } @@ -183,11 +191,42 @@ func (m *authenticationModel) Update(msg tea.Msg) (authModel authenticationModel return *m, tea.Sequence(m.cancelIsAuthenticated(), sendEvent(AuthModeSelected{})) case newPasswordCheck: - res := newPasswordCheckResult{challenge: msg.challenge} - if err := checkChallengeQuality(m.currentChallenge, msg.challenge); err != nil { - res.msg = err.Error() + currentChallenge := m.currentChallenge + return *m, func() tea.Msg { + res := newPasswordCheckResult{ctx: msg.ctx, challenge: msg.challenge} + if err := checkChallengeQuality(currentChallenge, msg.challenge); err != nil { + res.msg = err.Error() + } + return res + } + + case newPasswordCheckResult: + if m.clientType != Gdm { + // This may be handled by the current model, so don't return early. + break + } + + if msg.msg == "" { + return *m, sendEvent(isAuthenticatedRequestedSend{ + ctx: msg.ctx, + isAuthenticatedRequested: isAuthenticatedRequested{ + item: &authd.IARequest_AuthenticationData_Challenge{Challenge: msg.challenge}, + }, + }) + } + + errMsg, err := json.Marshal(msg.msg) + if err != nil { + return *m, sendEvent(pamError{ + status: pam.ErrSystem, + msg: fmt.Sprintf("could not encode %q error: %v", msg.msg, err), + }) } - return *m, sendEvent(res) + + return *m, sendEvent(isAuthenticatedResultReceived{ + access: brokers.AuthRetry, + msg: fmt.Sprintf(`{"message": %s}`, errMsg), + }) case isAuthenticatedRequested: log.Debugf(context.TODO(), "%#v", msg) @@ -219,26 +258,28 @@ func (m *authenticationModel) Update(msg tea.Msg) (authModel authenticationModel // At the point that we proceed with the actual authentication request in the goroutine, // there may still an authentication in progress, so send the request only after // we've completed the previous one(s). + clientType := m.clientType + currentLayout := m.currentLayout return *m, func() tea.Msg { authTracker.waitAndStart() + + challenge, hasChallenge := msg.item.(*authd.IARequest_AuthenticationData_Challenge) + if hasChallenge && clientType == Gdm && currentLayout == "newpassword" { + return newPasswordCheck{ctx: ctx, challenge: challenge.Challenge} + } + return isAuthenticatedRequestedSend{msg, ctx} } case isAuthenticatedRequestedSend: log.Debugf(context.TODO(), "%#v", msg) - // Store the current challenge, if present, for password verifications. - challenge, ok := msg.item.(*authd.IARequest_AuthenticationData_Challenge) - if !ok { - challenge = &authd.IARequest_AuthenticationData_Challenge{Challenge: ""} - } - m.currentChallenge = challenge.Challenge - // no challenge value, pass it as is - if err := msg.encryptChallengeIfPresent(m.encryptionKey); err != nil { + plainTextChallenge, err := msg.encryptChallengeIfPresent(m.encryptionKey) + if err != nil { return *m, sendEvent(pamError{status: pam.ErrSystem, msg: fmt.Sprintf("could not encrypt challenge payload: %v", err)}) } - return *m, sendIsAuthenticated(msg.ctx, m.client, m.currentSessionID, &authd.IARequest_AuthenticationData{Item: msg.item}) + return *m, sendIsAuthenticated(msg.ctx, m.client, m.currentSessionID, &authd.IARequest_AuthenticationData{Item: msg.item}, plainTextChallenge) case isAuthenticatedCancelled: log.Debugf(context.TODO(), "%#v", msg) @@ -251,8 +292,8 @@ func (m *authenticationModel) Update(msg tea.Msg) (authModel authenticationModel defer func() { // the returned authModel is a copy of function-level's `m` at this point! m := &authModel - if msg.access != brokers.AuthGranted && msg.access != brokers.AuthNext { - m.currentChallenge = "" + if msg.access == brokers.AuthGranted || msg.access == brokers.AuthNext { + m.currentChallenge = msg.challenge } m.cancelAuthFunc = nil @@ -348,6 +389,7 @@ func (m *authenticationModel) Compose(brokerID, sessionID string, encryptionKey m.currentSessionID = sessionID m.encryptionKey = encryptionKey m.cancelAuthFunc = nil + m.currentLayout = layout.Type m.errorMsg = "" @@ -406,6 +448,7 @@ func (m *authenticationModel) Reset() tea.Cmd { m.currentModel = nil m.currentSessionID = "" m.currentBrokerID = "" + m.currentLayout = "" return m.cancelIsAuthenticated() } @@ -430,22 +473,22 @@ func dataToMsg(data string) (string, error) { return r, nil } -func (authData *isAuthenticatedRequestedSend) encryptChallengeIfPresent(publicKey *rsa.PublicKey) error { +func (authData *isAuthenticatedRequestedSend) encryptChallengeIfPresent(publicKey *rsa.PublicKey) (string, error) { // no challenge value, pass it as is challenge, ok := authData.item.(*authd.IARequest_AuthenticationData_Challenge) if !ok { - return nil + return "", nil } ciphertext, err := rsa.EncryptOAEP(sha512.New(), rand.Reader, publicKey, []byte(challenge.Challenge), nil) if err != nil { - return err + return "", err } // encrypt it to base64 and replace the challenge with it base64Encoded := base64.StdEncoding.EncodeToString(ciphertext) authData.item = &authd.IARequest_AuthenticationData_Challenge{Challenge: base64Encoded} - return nil + return challenge.Challenge, nil } // wait waits for the current authentication to be completed. diff --git a/pam/internal/adapter/gdmmodel_test.go b/pam/internal/adapter/gdmmodel_test.go index 75a790427..8cfdab5a0 100644 --- a/pam/internal/adapter/gdmmodel_test.go +++ b/pam/internal/adapter/gdmmodel_test.go @@ -67,6 +67,14 @@ func TestGdmModel(t *testing.T) { }, nil), pam_test.WithUILayout(passwordUILayoutID, "Password authentication", pam_test.FormUILayout()), } + newPasswordUILayoutID := "NewPassword" + singleBrokerNewPasswordClientOptions := []pam_test.DummyClientOptions{ + pam_test.WithIgnoreSessionIDChecks(), + pam_test.WithAvailableBrokers([]*authd.ABResponse_BrokerInfo{ + firstBrokerInfo, + }, nil), + pam_test.WithUILayout(newPasswordUILayoutID, "New Password form", pam_test.NewPasswordUILayout()), + } multiBrokerClientOptions := append(slices.Clone(singleBrokerClientOptions), pam_test.WithAvailableBrokers([]*authd.ABResponse_BrokerInfo{ firstBrokerInfo, secondBrokerInfo, @@ -363,6 +371,253 @@ func TestGdmModel(t *testing.T) { msg: "Hi GDM, it's a pleasure to get you in!", }, }, + "New password changed after server-side user, broker and authMode selection": { + clientOptions: append(slices.Clone(singleBrokerNewPasswordClientOptions), + pam_test.WithGetPreviousBrokerReturn(firstBrokerInfo.Id, nil), + pam_test.WithIsAuthenticatedReturn(&authd.IAResponse{ + Access: brokers.AuthGranted, + }, nil), + ), + messages: []tea.Msg{ + tea.Sequence(tea.Tick(gdmPollFrequency*2, func(t time.Time) tea.Msg { + return userSelected{username: "daemon-selected-user-and-broker"} + }))(), + gdmTestWaitForStage{ + stage: pam_proto.Stage_challenge, + commands: []tea.Cmd{ + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "gdm-good-password", + }}), + }, + }, + }, + supportedLayouts: []*authd.UILayout{pam_test.NewPasswordUILayout()}, + wantUsername: "daemon-selected-user-and-broker", + wantSelectedBroker: firstBrokerInfo.Id, + wantGdmRequests: []gdm.RequestType{ + gdm.RequestType_uiLayoutCapabilities, + gdm.RequestType_changeStage, // -> broker Selection + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + }, + wantGdmEvents: []gdm.EventType{ + gdm.EventType_userSelected, + gdm.EventType_brokersReceived, + gdm.EventType_brokerSelected, + gdm.EventType_authModeSelected, + gdm.EventType_uiLayoutReceived, + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, + }, + wantStage: pam_proto.Stage_challenge, + wantGdmAuthRes: []*authd.IAResponse{{ + Access: brokers.AuthGranted, + }}, + wantExitStatus: PamSuccess{ + BrokerID: firstBrokerInfo.Id, + }, + }, + "New password changed with message after server-side user, broker and authMode selection": { + clientOptions: append(slices.Clone(singleBrokerNewPasswordClientOptions), + pam_test.WithGetPreviousBrokerReturn(firstBrokerInfo.Id, nil), + pam_test.WithIsAuthenticatedReturn(&authd.IAResponse{ + Access: brokers.AuthGranted, + Msg: `{"message": "Hi GDM, it's a pleasure to change your password!"}`, + }, nil), + ), + messages: []tea.Msg{ + tea.Sequence(tea.Tick(gdmPollFrequency*2, func(t time.Time) tea.Msg { + return userSelected{username: "daemon-selected-user-and-broker"} + }))(), + gdmTestWaitForStage{ + stage: pam_proto.Stage_challenge, + commands: []tea.Cmd{ + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "gdm-good-password", + }}), + }, + }, + }, + supportedLayouts: []*authd.UILayout{pam_test.NewPasswordUILayout()}, + wantUsername: "daemon-selected-user-and-broker", + wantSelectedBroker: firstBrokerInfo.Id, + wantGdmRequests: []gdm.RequestType{ + gdm.RequestType_uiLayoutCapabilities, + gdm.RequestType_changeStage, // -> broker Selection + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + }, + wantGdmEvents: []gdm.EventType{ + gdm.EventType_userSelected, + gdm.EventType_brokersReceived, + gdm.EventType_brokerSelected, + gdm.EventType_authModeSelected, + gdm.EventType_uiLayoutReceived, + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, + }, + wantStage: pam_proto.Stage_challenge, + wantGdmAuthRes: []*authd.IAResponse{{ + Access: brokers.AuthGranted, + Msg: "Hi GDM, it's a pleasure to change your password!", + }}, + wantExitStatus: PamSuccess{ + BrokerID: firstBrokerInfo.Id, + msg: "Hi GDM, it's a pleasure to change your password!", + }, + }, + "New password can't change because not respecting rules after server-side user, broker and authMode selection": { + clientOptions: append(slices.Clone(singleBrokerNewPasswordClientOptions), + pam_test.WithGetPreviousBrokerReturn(firstBrokerInfo.Id, nil), + pam_test.WithIsAuthenticatedReturn(&authd.IAResponse{ + Access: brokers.AuthGranted, + Msg: `{"message": "Hi GDM, it's a pleasure to change your password!"}`, + }, nil), + ), + messages: []tea.Msg{ + tea.Sequence(tea.Tick(gdmPollFrequency*2, func(t time.Time) tea.Msg { + return userSelected{username: "daemon-selected-user-and-broker"} + }))(), + gdmTestWaitForStage{ + stage: pam_proto.Stage_challenge, + commands: []tea.Cmd{ + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "newpass", + }}), + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "foolinux", + }}), + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "gdm-good-password", + }}), + }, + }, + }, + supportedLayouts: []*authd.UILayout{pam_test.NewPasswordUILayout()}, + wantUsername: "daemon-selected-user-and-broker", + wantSelectedBroker: firstBrokerInfo.Id, + wantGdmRequests: []gdm.RequestType{ + gdm.RequestType_uiLayoutCapabilities, + gdm.RequestType_changeStage, // -> broker Selection + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + }, + wantGdmEvents: []gdm.EventType{ + gdm.EventType_userSelected, + gdm.EventType_brokersReceived, + gdm.EventType_brokerSelected, + gdm.EventType_authModeSelected, + gdm.EventType_uiLayoutReceived, + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, // retry + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, // retry + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, // granted + }, + wantStage: pam_proto.Stage_challenge, + wantGdmAuthRes: []*authd.IAResponse{ + { + Access: brokers.AuthRetry, + Msg: "The password is shorter than 8 characters", + }, + { + Access: brokers.AuthRetry, + Msg: "The password fails the dictionary check - it is based on a dictionary word", + }, + { + Access: brokers.AuthGranted, + Msg: "Hi GDM, it's a pleasure to change your password!", + }, + }, + wantExitStatus: PamSuccess{ + BrokerID: firstBrokerInfo.Id, + msg: "Hi GDM, it's a pleasure to change your password!", + }, + }, + "New password can't change because matches previous after server-side user, broker and authMode selection": { + clientOptions: append(slices.Clone(singleBrokerClientOptions), + pam_test.WithGetPreviousBrokerReturn(firstBrokerInfo.Id, nil), + pam_test.WithUILayout(newPasswordUILayoutID, "New Password", pam_test.NewPasswordUILayout()), + pam_test.WithIsAuthenticatedReturn(&authd.IAResponse{ + Access: brokers.AuthNext, + Msg: `{"message": "Hi GDM, it's a pleasure to let you change your password!"}`, + }, nil), + ), + gdmEvents: []*gdm.EventData{ + gdm_test.SelectUserEvent("gdm-selected-user-broker-and-auth-mode"), + }, + messages: []tea.Msg{ + gdmTestWaitForStage{ + stage: pam_proto.Stage_authModeSelection, + events: []*gdm.EventData{ + gdm_test.AuthModeSelectedEvent(passwordUILayoutID), + }, + commands: []tea.Cmd{ + sendEvent(gdmTestWaitForStage{ + stage: pam_proto.Stage_challenge, + commands: []tea.Cmd{ + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "gdm-repeated-password", + }}), + sendEvent(gdmTestWaitForStage{ + stage: pam_proto.Stage_authModeSelection, + events: []*gdm.EventData{ + gdm_test.AuthModeSelectedEvent(newPasswordUILayoutID), + }, + commands: []tea.Cmd{ + sendEvent(gdmTestSendAuthDataWhenReady{&authd.IARequest_AuthenticationData_Challenge{ + Challenge: "gdm-repeated-password", + }}), + }, + }), + }, + }), + }, + }, + }, + supportedLayouts: []*authd.UILayout{ + pam_test.FormUILayout(), + pam_test.NewPasswordUILayout(), + }, + wantUsername: "gdm-selected-user-broker-and-auth-mode", + wantSelectedBroker: firstBrokerInfo.Id, + wantGdmRequests: []gdm.RequestType{ + gdm.RequestType_uiLayoutCapabilities, + gdm.RequestType_changeStage, // -> broker Selection + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + gdm.RequestType_changeStage, // -> authMode Selection + gdm.RequestType_changeStage, // -> challenge + }, + wantGdmEvents: []gdm.EventType{ + gdm.EventType_userSelected, + gdm.EventType_brokersReceived, + gdm.EventType_brokerSelected, + gdm.EventType_authModeSelected, + gdm.EventType_uiLayoutReceived, + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, // retry + gdm.EventType_authModeSelected, + gdm.EventType_startAuthentication, + gdm.EventType_authEvent, // retry + gdm.EventType_startAuthentication, + }, + wantStage: pam_proto.Stage_challenge, + wantGdmAuthRes: []*authd.IAResponse{ + { + Access: brokers.AuthNext, + Msg: "Hi GDM, it's a pleasure to let you change your password!", + }, + { + Access: brokers.AuthRetry, + Msg: "The password is the same as the old one", + }, + }, + wantExitStatus: gdmTestEarlyStopExitStatus, + }, "Authentication is ignored if not requested by model first": { clientOptions: append(slices.Clone(singleBrokerClientOptions), pam_test.WithIsAuthenticatedWantChallenge("gdm-good-password")), @@ -502,7 +757,7 @@ func TestGdmModel(t *testing.T) { gdm.EventType_startAuthentication, gdm.EventType_authEvent, // retry gdm.EventType_startAuthentication, - gdm.EventType_authEvent, // denied + gdm.EventType_authEvent, // granted }, wantMessages: []tea.Msg{ startAuthentication{}, diff --git a/pam/internal/adapter/nativemodel.go b/pam/internal/adapter/nativemodel.go index a9b4ffaed..4639734c6 100644 --- a/pam/internal/adapter/nativemodel.go +++ b/pam/internal/adapter/nativemodel.go @@ -723,7 +723,7 @@ func (m nativeModel) newPasswordChallenge(previousChallenge *string) tea.Cmd { } if previousChallenge == nil { - return sendEvent(newPasswordCheck{challenge}) + return sendEvent(newPasswordCheck{challenge: challenge}) } if challenge != *previousChallenge { err := m.sendError("Password entries don't match") diff --git a/pam/internal/adapter/newpasswordmodel.go b/pam/internal/adapter/newpasswordmodel.go index bf40566fe..4ca0dc2e8 100644 --- a/pam/internal/adapter/newpasswordmodel.go +++ b/pam/internal/adapter/newpasswordmodel.go @@ -102,7 +102,7 @@ func (m newPasswordModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // First entry is focused if m.focusIndex == 0 { // Check password quality - return m, sendEvent(newPasswordCheck{m.passwordEntries[0].Value()}) + return m, sendEvent(newPasswordCheck{challenge: m.passwordEntries[0].Value()}) } // Second entry is focused diff --git a/pam/internal/pam_test/pam-client-dummy.go b/pam/internal/pam_test/pam-client-dummy.go index 07b9a320e..a10f7b22b 100644 --- a/pam/internal/pam_test/pam-client-dummy.go +++ b/pam/internal/pam_test/pam-client-dummy.go @@ -590,12 +590,13 @@ func QrCodeUILayout() *authd.UILayout { // NewPasswordUILayout returns an [authd.UILayout] for new password forms. func NewPasswordUILayout() *authd.UILayout { required, optional := "required", "optional" - requiredWithBooleans := "required:true,false" + optionalWithBooleans := "optional:true,false" + supportedEntries := "optional:chars,chars_password" return &authd.UILayout{ - Type: "newpassword", - Content: &required, - Wait: &requiredWithBooleans, - Label: &optional, - Button: &optional, + Type: "newpassword", + Label: &required, + Entry: &supportedEntries, + Wait: &optionalWithBooleans, + Button: &optional, } }