From a1c23fef8c71cd4c4c5f77689e24c077c229b2c1 Mon Sep 17 00:00:00 2001 From: Theresa Henze Date: Tue, 19 Mar 2024 16:39:34 +0100 Subject: [PATCH] introduce event types to update/remove credentials Closes #10114 Signed-off-by: Theresa Henze --- .../release_notes/topics/26_0_0.adoc | 6 ++ .../server_admin/topics/admin-cli.adoc | 22 +++--- .../server_admin/topics/events/login.adoc | 16 +++- .../topics/changes/changes-26_0_0.adoc | 7 ++ .../partial-import-test-data/kcexport.json | 4 +- .../admin/messages/messages_en.properties | 10 ++- .../add/__tests__/mock-serverinfo.json | 4 + .../context/server-info/__tests__/mock.json | 4 + .../src/defs/eventTypes.ts | 6 +- .../java/org/keycloak/events/EventType.java | 15 ++++ .../DeleteCredentialAction.java | 31 +++++--- .../requiredactions/UpdatePassword.java | 15 +++- .../requiredactions/UpdateTotp.java | 9 ++- .../requiredactions/WebAuthnRegister.java | 43 ++++++++--- .../EmailEventListenerProviderFactory.java | 2 +- .../account/AccountCredentialResource.java | 8 +- .../services/resources/account/Constants.java | 2 +- .../account/AccountRestServiceTest.java | 30 +++++--- ...ppInitiatedActionDeleteCredentialTest.java | 21 ++++- .../AppInitiatedActionResetPasswordTest.java | 7 ++ .../AppInitiatedActionTotpSetupTest.java | 50 +++++++++--- .../RequiredActionMultipleActionsTest.java | 17 +++- .../actions/RequiredActionPriorityTest.java | 20 +++-- .../RequiredActionResetPasswordTest.java | 7 +- .../actions/RequiredActionTotpSetupTest.java | 77 +++++++++++++++---- .../kerberos/KerberosStandaloneTest.java | 5 +- .../ldap/LDAPProvidersIntegrationTest.java | 4 +- .../storage/UserStorageOTPTest.java | 8 +- .../testsuite/forms/BruteForceTest.java | 4 +- .../forms/LevelOfAssuranceFlowTest.java | 17 ++-- .../keycloak/testsuite/forms/LoginTest.java | 14 ++-- .../testsuite/forms/ResetPasswordTest.java | 31 +++++++- .../org/keycloak/testsuite/forms/SSOTest.java | 4 +- .../webauthn/WebAuthnIdlessTest.java | 15 +++- .../WebAuthnRegisterAndLoginTest.java | 32 ++++++-- .../WebAuthnOtherSettingsTest.java | 8 ++ .../import/testrealm-keycloak-6146-error.json | 2 +- .../import/testrealm-keycloak-6146.json | 2 +- .../email/html/event-remove_credential.ftl | 4 + .../email/html/event-update_credential.ftl | 4 + .../email/messages/messages_en.properties | 6 ++ .../email/text/event-remove_credential.ftl | 2 + .../email/text/event-update_credential.ftl | 2 + 43 files changed, 464 insertions(+), 133 deletions(-) create mode 100644 themes/src/main/resources/theme/base/email/html/event-remove_credential.ftl create mode 100644 themes/src/main/resources/theme/base/email/html/event-update_credential.ftl create mode 100644 themes/src/main/resources/theme/base/email/text/event-remove_credential.ftl create mode 100644 themes/src/main/resources/theme/base/email/text/event-update_credential.ftl diff --git a/docs/documentation/release_notes/topics/26_0_0.adoc b/docs/documentation/release_notes/topics/26_0_0.adoc index a3f56c1f3b..0a8df6ee73 100644 --- a/docs/documentation/release_notes/topics/26_0_0.adoc +++ b/docs/documentation/release_notes/topics/26_0_0.adoc @@ -162,3 +162,9 @@ In {project_name} 26, this feature is enabled by default. This means that all us It is possible to revert this behavior to the previous state by disabling the feature. Follow the `Volatile user sessions` section in https://www.keycloak.org/server/caching[Configuring distributed caches] guide for more details. For information on how to upgrade, see the link:{upgradingguide_link}[{upgradingguide_name}]. + += New generalized event types for credentials + +There are now generalized events for updating (`UPDATE_CREDENTIAL`) and removing (`REMOVE_CREDENTIAL`) a credential. The credential type is described in the `credential_type` attribute of the events. The new event types are supported by the Email Event Listener. + +The following event types are now deprecated and will be removed in a future version: `UPDATE_PASSWORD`, `UPDATE_PASSWORD_ERROR`, `UPDATE_TOTP`, `UPDATE_TOTP_ERROR`, `REMOVE_TOTP`, `REMOVE_TOTP_ERROR` diff --git a/docs/documentation/server_admin/topics/admin-cli.adoc b/docs/documentation/server_admin/topics/admin-cli.adoc index 837deae50c..91caab17a2 100644 --- a/docs/documentation/server_admin/topics/admin-cli.adoc +++ b/docs/documentation/server_admin/topics/admin-cli.adoc @@ -99,7 +99,7 @@ When you log in with the Admin CLI, you specify: * A realm * A user name -Another option is to specify a clientId only, which creates a unique service account for you to use. +Another option is to specify a clientId only, which creates a unique service account for you to use. When you log in using a user name, use a password for the specified user. When you log in using a clientId, you need the client secret only, not the user password. You can also use the `Signed JWT` rather than the client secret. @@ -119,7 +119,7 @@ The second mechanism authenticates each command invocation for the duration of t For example, when performing an operation, specify all the information required for authentication. [options="nowrap",subs="attributes+"] ---- -$ kcadm.sh get realms --no-config --server http://localhost:8080{kc_base_path} --realm master --user admin +$ kcadm.sh get realms --no-config --server http://localhost:8080{kc_base_path} --realm master --user admin ---- Run the `kcadm.sh help` command for more information on using the Admin CLI. @@ -131,9 +131,9 @@ If you do not specify the --password option (it is generally recommended to not [[_working_with_alternative_configurations]] === Working with alternative configurations -By default, the Admin CLI maintains a configuration file named `kcadm.config`. {project_name} places this file in the user's home directory. -In Linux-based systems, the full pathname is `$HOME/.keycloak/kcadm.config`. -In Windows, the full pathname is `%HOMEPATH%\.keycloak\kcadm.config`. +By default, the Admin CLI maintains a configuration file named `kcadm.config`. {project_name} places this file in the user's home directory. +In Linux-based systems, the full pathname is `$HOME/.keycloak/kcadm.config`. +In Windows, the full pathname is `%HOMEPATH%\.keycloak\kcadm.config`. You can use the `--config` option to point to a different file or location so you can maintain multiple authenticated sessions in parallel. @@ -199,7 +199,7 @@ The `create` and `update` commands send a JSON body to the server. You can use ` [NOTE] ==== -The value in name=value pairs used in --set, -s options, are assumed to be JSON. If it cannot be parsed as valid JSON, then it will be sent to the server as a text value. +The value in name=value pairs used in --set, -s options, are assumed to be JSON. If it cannot be parsed as valid JSON, then it will be sent to the server as a text value. If the value is enclosed in quotes after shell processing, but is not valid JSON, the quotes will be stripped and the rest of the value will be sent as text. This behavior is deprecated, please consider specifying your value without quotes or a valid JSON string literal with double quotes. ==== @@ -492,13 +492,13 @@ You can turn on storage for all available ERROR events, not including auditing e + [options="nowrap"] ---- -$ kcadm.sh update events/config -r demorealm -s eventsEnabled=true -s 'enabledEventTypes=["LOGIN_ERROR","REGISTER_ERROR","LOGOUT_ERROR","CODE_TO_TOKEN_ERROR","CLIENT_LOGIN_ERROR","FEDERATED_IDENTITY_LINK_ERROR","REMOVE_FEDERATED_IDENTITY_ERROR","UPDATE_EMAIL_ERROR","UPDATE_PROFILE_ERROR","UPDATE_PASSWORD_ERROR","UPDATE_TOTP_ERROR","VERIFY_EMAIL_ERROR","REMOVE_TOTP_ERROR","SEND_VERIFY_EMAIL_ERROR","SEND_RESET_PASSWORD_ERROR","SEND_IDENTITY_PROVIDER_LINK_ERROR","RESET_PASSWORD_ERROR","IDENTITY_PROVIDER_FIRST_LOGIN_ERROR","IDENTITY_PROVIDER_POST_LOGIN_ERROR","CUSTOM_REQUIRED_ACTION_ERROR","EXECUTE_ACTIONS_ERROR","CLIENT_REGISTER_ERROR","CLIENT_UPDATE_ERROR","CLIENT_DELETE_ERROR"]' -s eventsExpiration=172800 +$ kcadm.sh update events/config -r demorealm -s eventsEnabled=true -s 'enabledEventTypes=["LOGIN_ERROR","REGISTER_ERROR","LOGOUT_ERROR","CODE_TO_TOKEN_ERROR","CLIENT_LOGIN_ERROR","FEDERATED_IDENTITY_LINK_ERROR","REMOVE_FEDERATED_IDENTITY_ERROR","UPDATE_EMAIL_ERROR","UPDATE_PROFILE_ERROR","UPDATE_PASSWORD_ERROR","UPDATE_TOTP_ERROR","UPDATE_CREDENTIAL_ERROR","VERIFY_EMAIL_ERROR","REMOVE_TOTP_ERROR","REMOVE_CREDENTIAL_ERROR","SEND_VERIFY_EMAIL_ERROR","SEND_RESET_PASSWORD_ERROR","SEND_IDENTITY_PROVIDER_LINK_ERROR","RESET_PASSWORD_ERROR","IDENTITY_PROVIDER_FIRST_LOGIN_ERROR","IDENTITY_PROVIDER_POST_LOGIN_ERROR","CUSTOM_REQUIRED_ACTION_ERROR","EXECUTE_ACTIONS_ERROR","CLIENT_REGISTER_ERROR","CLIENT_UPDATE_ERROR","CLIENT_DELETE_ERROR"]' -s eventsExpiration=172800 ---- * Windows: + [options="nowrap"] ---- -c:\> kcadm update events/config -r demorealm -s eventsEnabled=true -s "enabledEventTypes=[\"LOGIN_ERROR\",\"REGISTER_ERROR\",\"LOGOUT_ERROR\",\"CODE_TO_TOKEN_ERROR\",\"CLIENT_LOGIN_ERROR\",\"FEDERATED_IDENTITY_LINK_ERROR\",\"REMOVE_FEDERATED_IDENTITY_ERROR\",\"UPDATE_EMAIL_ERROR\",\"UPDATE_PROFILE_ERROR\",\"UPDATE_PASSWORD_ERROR\",\"UPDATE_TOTP_ERROR\",\"VERIFY_EMAIL_ERROR\",\"REMOVE_TOTP_ERROR\",\"SEND_VERIFY_EMAIL_ERROR\",\"SEND_RESET_PASSWORD_ERROR\",\"SEND_IDENTITY_PROVIDER_LINK_ERROR\",\"RESET_PASSWORD_ERROR\",\"IDENTITY_PROVIDER_FIRST_LOGIN_ERROR\",\"IDENTITY_PROVIDER_POST_LOGIN_ERROR\",\"CUSTOM_REQUIRED_ACTION_ERROR\",\"EXECUTE_ACTIONS_ERROR\",\"CLIENT_REGISTER_ERROR\",\"CLIENT_UPDATE_ERROR\",\"CLIENT_DELETE_ERROR\"]" -s eventsExpiration=172800 +c:\> kcadm update events/config -r demorealm -s eventsEnabled=true -s "enabledEventTypes=[\"LOGIN_ERROR\",\"REGISTER_ERROR\",\"LOGOUT_ERROR\",\"CODE_TO_TOKEN_ERROR\",\"CLIENT_LOGIN_ERROR\",\"FEDERATED_IDENTITY_LINK_ERROR\",\"REMOVE_FEDERATED_IDENTITY_ERROR\",\"UPDATE_EMAIL_ERROR\",\"UPDATE_PROFILE_ERROR\",\"UPDATE_PASSWORD_ERROR\",\"UPDATE_TOTP_ERROR\",\"UPDATE_CREDENTIAL_ERROR\",\"VERIFY_EMAIL_ERROR\",\"REMOVE_TOTP_ERROR\",\"REMOVE_CREDENTIAL_ERROR\",\"SEND_VERIFY_EMAIL_ERROR\",\"SEND_RESET_PASSWORD_ERROR\",\"SEND_IDENTITY_PROVIDER_LINK_ERROR\",\"RESET_PASSWORD_ERROR\",\"IDENTITY_PROVIDER_FIRST_LOGIN_ERROR\",\"IDENTITY_PROVIDER_POST_LOGIN_ERROR\",\"CUSTOM_REQUIRED_ACTION_ERROR\",\"EXECUTE_ACTIONS_ERROR\",\"CLIENT_REGISTER_ERROR\",\"CLIENT_UPDATE_ERROR\",\"CLIENT_DELETE_ERROR\"]" -s eventsExpiration=172800 ---- You can reset stored event types to *all available event types*. Setting the value to an empty list is the same as enumerating all. @@ -1218,7 +1218,7 @@ $ kcadm.sh remove-roles -r demorealm --uusername testuser --cclientid realm-mana [discrete] ==== Listing a user's sessions -. Identify the user's ID, +. Identify the user's ID, . Use the ID to compose an endpoint URI, such as `users/ID/sessions`. . Use the `get` command to retrieve a list of the user's sessions. + @@ -1406,7 +1406,7 @@ $ kcadm.sh get-roles -r demorealm --gname Group --available Use the `get-roles` command to list assigned, available, and effective client roles for a group. -. Specify the target group by name (`--gname` option) or ID (`--gid` option), +. Specify the target group by name (`--gname` option) or ID (`--gid` option), . Specify the client by the clientId attribute (`--cclientid` option) or ID (`--id` option) to list *assigned* client roles for the user. + For example: @@ -1895,7 +1895,7 @@ $ kcadm get "authentication/config/dd91611a-d25c-421a-87e2-227c18421833" -r demo ==== Updating configuration for an execution . Get the execution for the flow. -. Get the flow's `authenticationConfig` attribute. +. Get the flow's `authenticationConfig` attribute. . Note the config ID from the attribute. . Run the `update` command on the `authentication/config/ID` endpoint. diff --git a/docs/documentation/server_admin/topics/events/login.adoc b/docs/documentation/server_admin/topics/events/login.adoc index 33e7d5bd17..5348422989 100644 --- a/docs/documentation/server_admin/topics/events/login.adoc +++ b/docs/documentation/server_admin/topics/events/login.adoc @@ -123,15 +123,21 @@ image:images/search-user-event.png[Search user event] |Send Password Reset |{project_name} sends a password reset email. -|Update Password +|Update Password (deprecated) |The password for an account changes. -|Update TOTP +|Update Credential +|The password or (time-based) one-time Password (OTP/TOTP) settings for an account changes. + +|Update TOTP (deprecated) |The Time-based One-time Password (TOTP) settings for an account changes. -|Remove TOTP +|Remove TOTP (deprecated) |{project_name} removes TOTP from an account. +|Remove Credential +|{project_name} removes a credential from an account. + |Send Verify Email |{project_name} sends an email verification email. @@ -191,6 +197,8 @@ The Email Event Listener sends a message to the user's email address when an eve * Update Password. * Update Time-based One-time Password (TOTP). * Remove One-time Password (OTP). +* Update Credential. +* Remove Credential. The following conditions need to be met for an email to be sent: @@ -217,6 +225,6 @@ You can exclude events by using the `--spi-events-listener-email-exclude-events` [source,bash] ---- -kc.[sh|bat] --spi-events-listener-email-exclude-events=UPDATE_TOTP,REMOVE_TOTP +kc.[sh|bat] --spi-events-listener-email-exclude-events=UPDATE_CREDENTIAL,REMOVE_CREDENTIAL ---- diff --git a/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc index c5e5e86e6d..5029cfc863 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc @@ -242,3 +242,10 @@ As part of this change the following related configuration options for the SPI h - `--spi-login-protocol-openid-connect-suppress-logout-confirmation-screen` If you were still making use these options or the `redirect_uri` parameter for logout you should implement the link:https://openid.net/specs/openid-connect-rpinitiated-1_0.html[OpenID Connect RP-Initiated Logout specification] instead. + += New generalized event types for credentials + +There are now generalized events for updating (`UPDATE_CREDENTIAL`) and removing (`REMOVE_CREDENTIAL`) a credential. The credential type is described in the `credential_type` attribute of the events. +The new event types are supported by the Email Event Listener. + +The following event types are now deprecated and will be removed in a future version: `UPDATE_PASSWORD`, `UPDATE_PASSWORD_ERROR`, `UPDATE_TOTP`, `UPDATE_TOTP_ERROR`, `REMOVE_TOTP`, `REMOVE_TOTP_ERROR` diff --git a/js/apps/admin-ui/cypress/fixtures/partial-import-test-data/kcexport.json b/js/apps/admin-ui/cypress/fixtures/partial-import-test-data/kcexport.json index 5135e3b143..c66bb12f04 100644 --- a/js/apps/admin-ui/cypress/fixtures/partial-import-test-data/kcexport.json +++ b/js/apps/admin-ui/cypress/fixtures/partial-import-test-data/kcexport.json @@ -2134,7 +2134,7 @@ "smtpServer" : { }, "eventsEnabled" : false, "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "UPDATE_EMAIL", "REGISTER_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "REGISTER", "LOGOUT", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "CLIENT_REGISTER", "SEND_VERIFY_EMAIL_ERROR", "UPDATE_PASSWORD", "RESET_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "VERIFY_EMAIL", "SEND_RESET_PASSWORD_ERROR", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "CLIENT_UPDATE", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN" ], + "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "REMOVE_CREDENTIAL", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "UPDATE_EMAIL", "REGISTER_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "UPDATE_CREDENTIAL_ERROR", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "REGISTER", "LOGOUT", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "CLIENT_REGISTER", "SEND_VERIFY_EMAIL_ERROR", "UPDATE_CREDENTIAL", "UPDATE_PASSWORD" ,"RESET_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "REMOVE_CREDENTIAL_ERROR", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "VERIFY_EMAIL", "SEND_RESET_PASSWORD_ERROR", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "CLIENT_UPDATE", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN" ], "adminEventsEnabled" : true, "adminEventsDetailsEnabled" : true, "identityProviders" : [ { @@ -4210,7 +4210,7 @@ "smtpServer" : { }, "eventsEnabled" : false, "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "UPDATE_EMAIL", "REGISTER_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "REGISTER", "LOGOUT", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "CLIENT_REGISTER", "SEND_VERIFY_EMAIL_ERROR", "UPDATE_PASSWORD", "RESET_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "VERIFY_EMAIL", "SEND_RESET_PASSWORD_ERROR", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "CLIENT_UPDATE", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN" ], + "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "REMOVE_CREDENTIAL", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "UPDATE_EMAIL", "REGISTER_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "UPDATE_CREDENTIAL_ERROR", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "REGISTER", "LOGOUT", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "CLIENT_REGISTER", "SEND_VERIFY_EMAIL_ERROR", "UPDATE_CREDENTIAL", "UPDATE_PASSWORD", "RESET_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "REMOVE_CREDENTIAL_ERROR", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "VERIFY_EMAIL", "SEND_RESET_PASSWORD_ERROR", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "CLIENT_UPDATE", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN" ], "adminEventsEnabled" : true, "adminEventsDetailsEnabled" : true, "identityProviders" : [ { diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 899ff005e0..962de5dedd 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3241,4 +3241,12 @@ temporaryService=Temporary admin service account. Ensure it is replaced with a p addOrganizationAttributes.label=Add organization attributes addOrganizationAttributes.help=If enabled, the organization attributes will be available for each organization mapped to the token. disableConfirmUserTitle=Disable user? -disableConfirmUser=Are you sure you want to disable this user? \ No newline at end of file +disableConfirmUser=Are you sure you want to disable this user? +eventTypes.UPDATE_CREDENTIAL.name=Update credential +eventTypes.UPDATE_CREDENTIAL.description=Update credential +eventTypes.UPDATE_CREDENTIAL_ERROR.name=Update credential error +eventTypes.UPDATE_CREDENTIAL_ERROR.description=Update credential error +eventTypes.REMOVE_CREDENTIAL.name=Remove credential +eventTypes.REMOVE_CREDENTIAL.description=Remove credential +eventTypes.REMOVE_CREDENTIAL_ERROR.name=Remove credential error +eventTypes.REMOVE_CREDENTIAL_ERROR.description=Remove credential error \ No newline at end of file diff --git a/js/apps/admin-ui/src/clients/add/__tests__/mock-serverinfo.json b/js/apps/admin-ui/src/clients/add/__tests__/mock-serverinfo.json index d9299848a9..9e25394f15 100644 --- a/js/apps/admin-ui/src/clients/add/__tests__/mock-serverinfo.json +++ b/js/apps/admin-ui/src/clients/add/__tests__/mock-serverinfo.json @@ -7212,6 +7212,8 @@ "REMOVE_FEDERATED_IDENTITY_ERROR", "REMOVE_TOTP", "REMOVE_TOTP_ERROR", + "REMOVE_CREDENTIAL", + "REMOVE_CREDENTIAL_ERROR", "RESET_PASSWORD", "RESET_PASSWORD_ERROR", "RESTART_AUTHENTICATION", @@ -7234,6 +7236,8 @@ "UPDATE_EMAIL_ERROR", "UPDATE_PASSWORD", "UPDATE_PASSWORD_ERROR", + "UPDATE_CREDENTIAL", + "UPDATE_CREDENTIAL_ERROR", "UPDATE_PROFILE", "UPDATE_PROFILE_ERROR", "UPDATE_TOTP", diff --git a/js/apps/admin-ui/src/context/server-info/__tests__/mock.json b/js/apps/admin-ui/src/context/server-info/__tests__/mock.json index fb68699134..2c78ee19a4 100644 --- a/js/apps/admin-ui/src/context/server-info/__tests__/mock.json +++ b/js/apps/admin-ui/src/context/server-info/__tests__/mock.json @@ -7074,6 +7074,8 @@ "REMOVE_FEDERATED_IDENTITY_ERROR", "REMOVE_TOTP", "REMOVE_TOTP_ERROR", + "REMOVE_CREDENTIAL", + "REMOVE_CREDENTIAL_ERROR", "RESET_PASSWORD", "RESET_PASSWORD_ERROR", "RESTART_AUTHENTICATION", @@ -7096,6 +7098,8 @@ "UPDATE_EMAIL_ERROR", "UPDATE_PASSWORD", "UPDATE_PASSWORD_ERROR", + "UPDATE_CREDENTIAL", + "UPDATE_CREDENTIAL_ERROR", "UPDATE_PROFILE", "UPDATE_PROFILE_ERROR", "UPDATE_TOTP", diff --git a/js/libs/keycloak-admin-client/src/defs/eventTypes.ts b/js/libs/keycloak-admin-client/src/defs/eventTypes.ts index 288692bb42..892c835c14 100644 --- a/js/libs/keycloak-admin-client/src/defs/eventTypes.ts +++ b/js/libs/keycloak-admin-client/src/defs/eventTypes.ts @@ -84,6 +84,10 @@ type EventType = | "TOKEN_EXCHANGE" | "TOKEN_EXCHANGE_ERROR" | "PERMISSION_TOKEN" - | "PERMISSION_TOKEN_ERROR"; + | "PERMISSION_TOKEN_ERROR" + | "UPDATE_CREDENTIAL" + | "UPDATE_CREDENTIAL_ERROR" + | "REMOVE_CREDENTIAL" + | "REMOVE_CREDENTIAL_ERROR"; export default EventType; diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java index a203562d7b..16697c5141 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java @@ -62,16 +62,24 @@ public enum EventType implements EnumWithStableIndex { UPDATE_EMAIL_ERROR(0x10000 + UPDATE_EMAIL.getStableIndex(), true), UPDATE_PROFILE(11, true), UPDATE_PROFILE_ERROR(0x10000 + UPDATE_PROFILE.getStableIndex(), true), + + @Deprecated UPDATE_PASSWORD(12, true), + @Deprecated UPDATE_PASSWORD_ERROR(0x10000 + UPDATE_PASSWORD.getStableIndex(), true), + + @Deprecated UPDATE_TOTP(13, true), + @Deprecated UPDATE_TOTP_ERROR(0x10000 + UPDATE_TOTP.getStableIndex(), true), VERIFY_EMAIL(14, true), VERIFY_EMAIL_ERROR(0x10000 + VERIFY_EMAIL.getStableIndex(), true), VERIFY_PROFILE(15, true), VERIFY_PROFILE_ERROR(0x10000 + VERIFY_PROFILE.getStableIndex(), true), + @Deprecated REMOVE_TOTP(16, true), + @Deprecated REMOVE_TOTP_ERROR(0x10000 + REMOVE_TOTP.getStableIndex(), true), GRANT_CONSENT(17, true), @@ -169,6 +177,13 @@ public enum EventType implements EnumWithStableIndex { FEDERATED_IDENTITY_OVERRIDE_LINK(55, true), FEDERATED_IDENTITY_OVERRIDE_LINK_ERROR(0x10000 + FEDERATED_IDENTITY_OVERRIDE_LINK.getStableIndex(), true), + + UPDATE_CREDENTIAL(56, true), + UPDATE_CREDENTIAL_ERROR(0x10000 + UPDATE_CREDENTIAL.getStableIndex(), true), + + REMOVE_CREDENTIAL(57, true), + REMOVE_CREDENTIAL_ERROR(0x10000 + REMOVE_CREDENTIAL.getStableIndex(), true), + INVITE_ORG(60, true), INVITE_ORG_ERROR(0x10000 + INVITE_ORG.getStableIndex(), true); diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/DeleteCredentialAction.java b/services/src/main/java/org/keycloak/authentication/requiredactions/DeleteCredentialAction.java index 23736b07a7..11d2f0743d 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/DeleteCredentialAction.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/DeleteCredentialAction.java @@ -143,28 +143,31 @@ public class DeleteCredentialAction implements RequiredActionProvider, RequiredA context.challenge(challenge); } - private void setupEvent(CredentialModel credential, EventBuilder event) { - if (credential != null) { - if (OTPCredentialModel.TYPE.equals(credential.getType())) { - event.event(EventType.REMOVE_TOTP); - } - event.detail(Details.CREDENTIAL_TYPE, credential.getType()) - .detail(Details.CREDENTIAL_ID, credential.getId()) - .detail(Details.CREDENTIAL_USER_LABEL, credential.getUserLabel()); - } - } - @Override public void processAction(RequiredActionContext context) { EventBuilder event = context.getEvent(); + event.event(EventType.REMOVE_CREDENTIAL); + + EventBuilder deprecatedEvent = null; String credentialId = context.getAuthenticationSession().getClientNote(Constants.KC_ACTION_PARAMETER); CredentialModel credential = context.getUser().credentialManager().getStoredCredentialById(credentialId); - setupEvent(credential, event); + if (credential != null) { + event + .detail(Details.CREDENTIAL_TYPE, credential.getType()) + .detail(Details.CREDENTIAL_ID, credential.getId()) + .detail(Details.CREDENTIAL_USER_LABEL, credential.getUserLabel()); + if (OTPCredentialModel.TYPE.equals(credential.getType())) { + deprecatedEvent = event.clone().event(EventType.REMOVE_TOTP); + } + } try { CredentialDeleteHelper.removeCredential(context.getSession(), context.getUser(), credentialId, () -> getCurrentLoa(context.getSession(), context.getAuthenticationSession())); context.success(); + if (deprecatedEvent != null) { + deprecatedEvent.success(); + } } catch (WebApplicationException wae) { Response response = context.getSession().getProvider(LoginFormsProvider.class) @@ -174,6 +177,10 @@ public class DeleteCredentialAction implements RequiredActionProvider, RequiredA .createErrorPage(Response.Status.BAD_REQUEST); event.detail(Details.REASON, wae.getMessage()) .error(Errors.DELETE_CREDENTIAL_FAILED); + if (deprecatedEvent != null) { + deprecatedEvent.detail(Details.REASON, wae.getMessage()) + .error(Errors.DELETE_CREDENTIAL_FAILED); + } context.challenge(response); } } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java index 30063f3885..c1e97b6aea 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java @@ -45,6 +45,7 @@ import org.keycloak.models.RequiredActionConfigModel; import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.policy.MaxAuthAgePasswordPolicyProviderFactory; import org.keycloak.provider.ProviderConfigProperty; @@ -147,13 +148,18 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac AuthenticationSessionModel authSession = context.getAuthenticationSession(); UserModel user = context.getUser(); MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - event.event(EventType.UPDATE_PASSWORD); + + event.event(EventType.UPDATE_CREDENTIAL); + event.detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.PASSWORD); + EventBuilder deprecatedEvent = event.clone().event(EventType.UPDATE_PASSWORD); + String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); - EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR) + EventBuilder errorEvent = event.clone().event(EventType.UPDATE_CREDENTIAL_ERROR) .client(authSession.getClient()) .user(authSession.getAuthenticatedUser()); + EventBuilder deprecatedErrorEvent = errorEvent.clone().event(EventType.UPDATE_PASSWORD_ERROR); if (Validation.isBlank(passwordNew)) { Response challenge = context.form() @@ -162,6 +168,7 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); context.challenge(challenge); errorEvent.error(Errors.PASSWORD_MISSING); + deprecatedErrorEvent.error(Errors.PASSWORD_MISSING); return; } else if (!passwordNew.equals(passwordConfirm)) { Response challenge = context.form() @@ -170,6 +177,7 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); context.challenge(challenge); errorEvent.error(Errors.PASSWORD_CONFIRM_ERROR); + deprecatedErrorEvent.error(Errors.PASSWORD_CONFIRM_ERROR); return; } @@ -180,8 +188,10 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac try { user.credentialManager().updateCredential(UserCredentialModel.password(passwordNew, false)); context.success(); + deprecatedEvent.success(); } catch (ModelException me) { errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED); + deprecatedErrorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED); Response challenge = context.form() .setAttribute("username", authSession.getAuthenticatedUser().getUsername()) .setError(me.getMessage(), me.getParameters()) @@ -190,6 +200,7 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac return; } catch (Exception ape) { errorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED); + deprecatedErrorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED); Response challenge = context.form() .setAttribute("username", authSession.getAuthenticatedUser().getUsername()) .setError(ape.getMessage()) diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java index 6a2da55dee..34f3cb076f 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java @@ -27,6 +27,7 @@ import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.OTPCredentialProvider; +import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.models.KeycloakSession; @@ -55,7 +56,7 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory public InitiatedActionSupport initiatedActionSupport() { return InitiatedActionSupport.SUPPORTED; } - + @Override public void evaluateTriggers(RequiredActionContext context) { } @@ -71,7 +72,7 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory @Override public void processAction(RequiredActionContext context) { EventBuilder event = context.getEvent(); - event.event(EventType.UPDATE_TOTP); + event.event(EventType.UPDATE_CREDENTIAL); MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); String challengeResponse = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); @@ -80,6 +81,9 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory OTPPolicy policy = context.getRealm().getOTPPolicy(); OTPCredentialModel credentialModel = OTPCredentialModel.createFromPolicy(context.getRealm(), totpSecret, userLabel); + event.detail(Details.CREDENTIAL_TYPE, credentialModel.getType()); + + EventBuilder deprecatedEvent = event.clone().event(EventType.UPDATE_TOTP); if (Validation.isBlank(challengeResponse)) { Response challenge = context.form() .setAttribute("mode", mode) @@ -122,6 +126,7 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory } context.getAuthenticationSession().removeAuthNote(Constants.TOTP_SECRET_KEY); context.success(); + deprecatedEvent.success(); } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegister.java b/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegister.java index b6e0e9bc98..7dbd5c7876 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegister.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/WebAuthnRegister.java @@ -33,6 +33,8 @@ import jakarta.ws.rs.core.Response; import com.webauthn4j.WebAuthnRegistrationManager; import com.webauthn4j.data.AuthenticatorTransport; import org.jboss.logging.Logger; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; import org.keycloak.http.HttpRequest; import org.keycloak.WebAuthnConstants; import org.keycloak.authentication.AuthenticatorUtil; @@ -184,6 +186,15 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis return WebAuthnCredentialProviderFactory.PROVIDER_ID; } + /** + * Method to provide removal and deprecation hint + * @deprecated For compatibility sake as long as we use @link {@link EventType#UPDATE_PASSWORD} , {@link EventType#UPDATE_TOTP} a.s.o. + */ + @Deprecated + protected EventType getOriginalEventTypeForBackwardsCompatibility(RequiredActionContext context) { + return context.getEvent().getEvent().getType(); + } + @Override public void processAction(RequiredActionContext context) { @@ -195,12 +206,15 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis return; } - context.getEvent().detail(Details.CREDENTIAL_TYPE, getCredentialType()); + EventType originalEventType = getOriginalEventTypeForBackwardsCompatibility(context); + context.getEvent() + .event(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, getCredentialType()); // receive error from navigator.credentials.create() String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR); if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) { - setErrorResponse(context, WEBAUTHN_ERROR_REGISTER_VERIFICATION, errorMsgFromWebAuthnApi); + setErrorResponse(context, WEBAUTHN_ERROR_REGISTER_VERIFICATION, errorMsgFromWebAuthnApi, originalEventType); return; } @@ -269,14 +283,15 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis .detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, publicKeyCredentialId) .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, label) .detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, aaguid); + context.getEvent().clone().event(originalEventType).success(); context.success(); } catch (WebAuthnException wae) { if (logger.isDebugEnabled()) logger.debug(wae.getMessage(), wae); - setErrorResponse(context, WEBAUTHN_ERROR_REGISTRATION, wae.getMessage()); + setErrorResponse(context, WEBAUTHN_ERROR_REGISTRATION, wae.getMessage(), originalEventType); return; } catch (Exception e) { if (logger.isDebugEnabled()) logger.debug(e.getMessage(), e); - setErrorResponse(context, WEBAUTHN_ERROR_REGISTRATION, e.getMessage()); + setErrorResponse(context, WEBAUTHN_ERROR_REGISTRATION, e.getMessage(), originalEventType); return; } } @@ -387,15 +402,17 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis // NOP } - private void setErrorResponse(RequiredActionContext context, final String errorCase, final String errorMessage) { + private void setErrorResponse(RequiredActionContext context, final String errorCase, final String errorMessage, @Deprecated final EventType originalEventType) { Response errorResponse = null; switch (errorCase) { case WEBAUTHN_ERROR_REGISTER_VERIFICATION: logger.warnv("WebAuthn API .create() response validation failure. {0}", errorMessage); - context.getEvent() + EventBuilder registerVerificationEvent = context.getEvent() .detail(REG_ERR_LABEL, errorCase) - .detail(REG_ERR_DETAIL_LABEL, errorMessage) - .error(Errors.INVALID_USER_CREDENTIALS); + .detail(REG_ERR_DETAIL_LABEL, errorMessage); + EventBuilder deprecatedRegisterVerificationEvent = registerVerificationEvent.clone().event(originalEventType); + registerVerificationEvent.error(Errors.INVALID_USER_CREDENTIALS); + deprecatedRegisterVerificationEvent.error(Errors.INVALID_USER_CREDENTIALS); errorResponse = context.form() .setError(errorCase, errorMessage) .setAttribute(WEB_AUTHN_TITLE_ATTR, WEBAUTHN_REGISTER_TITLE) @@ -404,10 +421,12 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis break; case WEBAUTHN_ERROR_REGISTRATION: logger.warn(errorCase); - context.getEvent() - .detail(REG_ERR_LABEL, errorCase) - .detail(REG_ERR_DETAIL_LABEL, errorMessage) - .error(Errors.INVALID_REGISTRATION); + EventBuilder registrationEvent = context.getEvent() + .detail(REG_ERR_LABEL, errorCase) + .detail(REG_ERR_DETAIL_LABEL, errorMessage); + EventBuilder deprecatedRegistrationEvent = registrationEvent.clone().event(originalEventType); + deprecatedRegistrationEvent.error(Errors.INVALID_REGISTRATION); + registrationEvent.error(Errors.INVALID_REGISTRATION); errorResponse = context.form() .setError(errorCase, errorMessage) .setAttribute(WEB_AUTHN_TITLE_ATTR, WEBAUTHN_REGISTER_TITLE) diff --git a/services/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java b/services/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java index c0b4d00cc1..f31bb45472 100755 --- a/services/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java +++ b/services/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java @@ -44,7 +44,7 @@ public class EmailEventListenerProviderFactory implements EventListenerProviderF public static final String ID = "email"; static { - Collections.addAll(SUPPORTED_EVENTS, EventType.LOGIN_ERROR, EventType.UPDATE_PASSWORD, EventType.REMOVE_TOTP, EventType.UPDATE_TOTP); + Collections.addAll(SUPPORTED_EVENTS, EventType.LOGIN_ERROR, EventType.UPDATE_PASSWORD, EventType.REMOVE_TOTP, EventType.UPDATE_TOTP, EventType.UPDATE_CREDENTIAL, EventType.REMOVE_CREDENTIAL); } private Set includedEvents = new HashSet<>(); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java b/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java index 177b2c4f6a..3e96a8b455 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java @@ -307,10 +307,14 @@ public class AccountCredentialResource { realm.getName()); CredentialModel credential = CredentialDeleteHelper.removeCredential(session, user, credentialId, this::getCurrentAuthenticatedLevel); - if (credential != null && OTPCredentialModel.TYPE.equals(credential.getType())) { - event.event(EventType.REMOVE_TOTP) + if (credential != null) { + event.event(EventType.REMOVE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, credential.getType()) .detail(Details.SELECTED_CREDENTIAL_ID, credentialId) .detail(Details.CREDENTIAL_USER_LABEL, credential.getUserLabel()); + if (OTPCredentialModel.TYPE.equals(credential.getType())) { + event.clone().event(EventType.REMOVE_TOTP).success(); + } event.success(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/account/Constants.java b/services/src/main/java/org/keycloak/services/resources/account/Constants.java index b443256f31..60389c3556 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/Constants.java +++ b/services/src/main/java/org/keycloak/services/resources/account/Constants.java @@ -29,7 +29,7 @@ public class Constants { public static final EventType[] EXPOSED_LOG_EVENTS = { EventType.LOGIN, EventType.LOGOUT, EventType.REGISTER, EventType.REMOVE_FEDERATED_IDENTITY, EventType.REMOVE_TOTP, EventType.SEND_RESET_PASSWORD, - EventType.SEND_VERIFY_EMAIL, EventType.FEDERATED_IDENTITY_LINK, EventType.UPDATE_EMAIL, EventType.UPDATE_PASSWORD, EventType.UPDATE_PROFILE, EventType.UPDATE_TOTP, EventType.VERIFY_EMAIL + EventType.SEND_VERIFY_EMAIL, EventType.FEDERATED_IDENTITY_LINK, EventType.UPDATE_EMAIL, EventType.UPDATE_CREDENTIAL, EventType.REMOVE_CREDENTIAL, EventType.UPDATE_PROFILE, EventType.UPDATE_PASSWORD, EventType.UPDATE_PROFILE, EventType.UPDATE_TOTP, EventType.VERIFY_EMAIL }; public static final Set EXPOSED_LOG_DETAILS = new HashSet<>(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index bb69350be5..0397e757dd 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -107,7 +107,7 @@ import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL; * @author Stian Thorgersen */ public class AccountRestServiceTest extends AbstractRestServiceTest { - + @Rule public AssertEvents events = new AssertEvents(this); @@ -227,7 +227,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { UserRepresentation user = getUser(false); assertNull(user.getUserProfileMetadata()); } - + protected static UserProfileAttributeMetadata getUserProfileAttributeMetadata(UserRepresentation user, String attName) { if(user.getUserProfileMetadata() == null) return null; @@ -238,7 +238,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { } return null; } - + protected static UserProfileAttributeMetadata assertUserProfileAttributeMetadata(UserRepresentation user, String attName, String displayName, boolean required, boolean readOnly) { UserProfileAttributeMetadata uam = getUserProfileAttributeMetadata(user, attName); @@ -310,7 +310,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { } } - + /** * Reproducer for bugs KEYCLOAK-17424 and KEYCLOAK-17582 */ @@ -323,13 +323,13 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { realmRep.setRegistrationEmailAsUsername(false); adminClient.realm("test").update(realmRep); - + //set flag over adminClient to initial value UserResource userResource = adminClient.realm("test").users().get(user.getId()); org.keycloak.representations.idm.UserRepresentation ur = userResource.toRepresentation(); ur.setEmailVerified(true); userResource.update(ur); - //make sure flag is correct before the test + //make sure flag is correct before the test user = getUser(); assertEquals(true, user.isEmailVerified()); @@ -339,7 +339,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { assertEquals(originalEmail, user.getEmail()); assertEquals(true, user.isEmailVerified()); - + // Update email - flag must be reset to false user.setEmail("bobby@localhost"); user = updateAndGet(user); @@ -402,7 +402,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { .detail(Details.UPDATED_LAST_NAME, "Simpsons") .assertEvent(); events.assertEmpty(); - + } finally { RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); realmRep.setEditUsernameAllowed(true); @@ -417,7 +417,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { assertEquals(204, response.getStatus()); } } - + @Test public void testUpdateProfile() throws IOException { String userProfileCfg = "{\"attributes\": [" @@ -455,7 +455,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { assertEquals("val1", user.getAttributes().get("attr1").get(0)); assertEquals(1, user.getAttributes().get("attr2").size()); assertEquals("val2", user.getAttributes().get("attr2").get(0)); - + // Update attributes user.getAttributes().remove("attr1"); user.getAttributes().get("attr2").add("val3"); @@ -637,7 +637,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { throw e; } } - + protected UserRepresentation updateAndGet(UserRepresentation user) throws IOException { SimpleHttp a = SimpleHttpDefault.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user); try { @@ -886,6 +886,14 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { .user(user.toRepresentation().getId()) .detail(Details.SELECTED_CREDENTIAL_ID, otpCredential.getId()) .detail(Details.CREDENTIAL_USER_LABEL, "totpCredentialUserLabel") + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .assertEvent(); + events.expect(EventType.REMOVE_CREDENTIAL) + .client("account") + .user(user.toRepresentation().getId()) + .detail(Details.SELECTED_CREDENTIAL_ID, otpCredential.getId()) + .detail(Details.CREDENTIAL_USER_LABEL, "totpCredentialUserLabel") + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) .assertEvent(); events.assertEmpty(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java index e1877990d3..0c96b5eafb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionDeleteCredentialTest.java @@ -91,7 +91,7 @@ public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiated } @Test - public void removeTotpSuccess() throws Exception { + public void removeOtpSuccess() throws Exception { String credentialId = getCredentialIdByType(OTPCredentialModel.TYPE); oauth.kcAction(getKcActionParamForDeleteCredential(credentialId)); @@ -113,10 +113,16 @@ public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiated .detail(Details.CREDENTIAL_ID, credentialId) .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) .assertEvent(); + events.expect(EventType.REMOVE_CREDENTIAL) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.CREDENTIAL_ID, credentialId) + .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) + .assertEvent(); } @Test - public void removeTotpCancel() throws Exception { + public void removeOtpCancel() throws Exception { String credentialId = getCredentialIdByType(OTPCredentialModel.TYPE); loginPasswordAndOtp(); @@ -155,7 +161,7 @@ public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiated errorPage.assertCurrent(); - events.expect(EventType.CUSTOM_REQUIRED_ACTION) + events.expect(EventType.REMOVE_CREDENTIAL) .user(userId) .detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) .detail(Details.CREDENTIAL_ID, credentialId) @@ -218,7 +224,7 @@ public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiated } @Test - public void removeTotpCustomLabel() throws Exception { + public void removeOtpCustomLabel() throws Exception { String credentialId = getCredentialIdByType(OTPCredentialModel.TYPE); testRealm().users().get(userId).setCredentialUserLabel(credentialId, "custom-otp-authenticator"); @@ -242,6 +248,13 @@ public class AppInitiatedActionDeleteCredentialTest extends AbstractAppInitiated .detail(Details.CREDENTIAL_USER_LABEL, "custom-otp-authenticator") .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) .assertEvent(); + events.expect(EventType.REMOVE_CREDENTIAL) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.CREDENTIAL_ID, credentialId) + .detail(Details.CREDENTIAL_USER_LABEL, "custom-otp-authenticator") + .detail(Details.CUSTOM_REQUIRED_ACTION, DeleteCredentialAction.PROVIDER_ID) + .assertEvent(); } private String getCredentialIdByType(String type) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java index 001a768184..2fdcf54878 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java @@ -28,6 +28,7 @@ import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -107,6 +108,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct }); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); assertKcActionStatus(SUCCESS); @@ -145,6 +147,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct changePasswordPage.changePassword("new-password", "new-password"); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); assertKcActionStatus(SUCCESS); } @@ -188,6 +191,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct changePasswordPage.changePassword("new-password", "new-password"); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); assertKcActionStatus(SUCCESS); } finally { // reset password policy to previous state @@ -232,6 +236,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct changePasswordPage.changePassword("new-password", "new-password"); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); assertKcActionStatus(SUCCESS); } @@ -261,6 +266,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct changePasswordPage.changePassword("All Right Then, Keep Your Secrets", "All Right Then, Keep Your Secrets"); events.expectLogout(event2.getSessionId()).detail(Details.LOGOUT_TRIGGERED_BY_REQUIRED_ACTION, UserModel.RequiredAction.UPDATE_PASSWORD.name()).assertEvent(); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); assertKcActionStatus(SUCCESS); sessions = testUser.getUserSessions(); @@ -289,6 +295,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct changePasswordPage.uncheckLogoutSessions(); changePasswordPage.changePassword("All Right Then, Keep Your Secrets", "All Right Then, Keep Your Secrets"); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); assertKcActionStatus(SUCCESS); assertEquals(2, testUser.getUserSessions().size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTotpSetupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTotpSetupTest.java index 95681e6c9e..7f17c84fc1 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTotpSetupTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTotpSetupTest.java @@ -111,12 +111,21 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); events.poll(); // skip to totp event - String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent() + String authSessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setuptotp").assertEvent() + .getDetails().get(Details.CODE_ID); + String authSessionId2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setuptotp").assertEvent() .getDetails().get(Details.CODE_ID); assertKcActionStatus(SUCCESS); - events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent(); + assertEquals(authSessionId1, authSessionId2); + events.expectLogin().user(userId).session(authSessionId2).detail(Details.USERNAME, "setuptotp").assertEvent(); } @Test @@ -346,17 +355,22 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT totpPage.configure(totp.generateTOTP(totpSecret)); - String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() + String authSessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() + .getDetails().get(Details.CODE_ID); + String authSessionId2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() .getDetails().get(Details.CODE_ID); assertKcActionStatus(SUCCESS); - EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent(); + assertEquals(authSessionId1, authSessionId2); + EventRepresentation loginEvent = events.expectLogin().session(authSessionId2).assertEvent(); OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); - events.expectLogout(authSessionId).assertEvent(); + events.expectLogout(authSessionId2).assertEvent(); setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp); @@ -389,7 +403,14 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT assertKcActionStatus(SUCCESS); events.poll(); - events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_TOTP) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setuptotp2").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setuptotp2").assertEvent(); EventRepresentation loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent(); @@ -448,12 +469,17 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT TimeBasedOTP timeBased = new TimeBasedOTP(HmacOTP.HMAC_SHA1, 8, 30, 1); totpPage.configure(timeBased.generateTOTP(totpSecret)); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() + String sessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() + .getDetails().get(Details.CODE_ID); + String sessionId2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() .getDetails().get(Details.CODE_ID); assertKcActionStatus(SUCCESS); - EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent(); + assertEquals(sessionId1, sessionId2); + EventRepresentation loginEvent = events.expectLogin().session(sessionId2).assertEvent(); OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); @@ -503,12 +529,16 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT HmacOTP otpgen = new HmacOTP(6, HmacOTP.HMAC_SHA1, 1); totpPage.configure(otpgen.generateHOTP(totpSecret, 0)); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() + String sessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() .getDetails().get(Details.CODE_ID); + String sessionId2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() + .getDetails().get(Details.CODE_ID); //RequestType reqType = appPage.getRequestType(); assertKcActionStatus(SUCCESS); - EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent(); + EventRepresentation loginEvent = events.expectLogin().session(sessionId1).assertEvent(); OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java index 9b8b3a9d4d..db7729a063 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java @@ -23,6 +23,8 @@ import org.junit.Test; import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; @@ -86,11 +88,20 @@ public class RequiredActionMultipleActionsTest extends AbstractTestRealmKeycloak public String updatePassword(String codeId) { changePasswordPage.changePassword("new-password", "new-password"); - AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_PASSWORD); + AssertEvents.ExpectedEvent expectedEvent1 = events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE); if (codeId != null) { - expectedEvent.detail(Details.CODE_ID, codeId); + expectedEvent1.detail(Details.CODE_ID, codeId); } - return expectedEvent.assertEvent().getDetails().get(Details.CODE_ID); + EventRepresentation eventRep1 = expectedEvent1.assertEvent(); + + AssertEvents.ExpectedEvent expectedEvent2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE); + if (codeId != null) { + expectedEvent2.detail(Details.CODE_ID, codeId); + } + EventRepresentation eventRep2 = expectedEvent2.assertEvent(); + + Assert.assertEquals(eventRep1.getDetails().get(Details.CODE_ID), eventRep2.getDetails().get(Details.CODE_ID)); + return eventRep2.getDetails().get(Details.CODE_ID); } public String updateProfile(String codeId) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java index bcd4f9e892..057315bc1d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java @@ -33,6 +33,8 @@ import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.models.credential.OTPCredentialModel; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; @@ -139,7 +141,8 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { // Second, change password changePasswordPage.assertCurrent(); changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); // Finally, update profile updateProfilePage.assertCurrent(); @@ -182,7 +185,8 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { // First, change password changePasswordPage.assertCurrent(); changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); // Second, update profile updateProfilePage.assertCurrent(); @@ -236,6 +240,7 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { changePasswordPage.assertCurrent(); changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).assertEvent(); // Second, update profile updateProfilePage.assertCurrent(); @@ -299,7 +304,8 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { // First, change password changePasswordPage.assertCurrent(); changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); // Second, update profile updateProfilePage.assertCurrent(); @@ -395,7 +401,8 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { // change password changePasswordPage.assertCurrent(); changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); // CONFIGURE_TOTP totpPage.assertCurrent(); @@ -407,7 +414,8 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { TimeBasedOTP totp = new TimeBasedOTP(); totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()), "userLabel"); - events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_TOTP).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); // Logged in appPage.assertCurrent(); @@ -448,7 +456,7 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { appPage.assertCurrent(); assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE)); - events.expectLogin().assertEvent(); + events.expect(EventType.UPDATE_CREDENTIAL).assertEvent(); } private void enableRequiredActionForUser(final RequiredAction requiredAction) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java index 6a57f85125..404b4f8d1c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java @@ -30,6 +30,7 @@ import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -106,7 +107,8 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe changePasswordPage.changePassword("new-password", "new-password"); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -159,7 +161,8 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe .assertEvent(); } - events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); EventRepresentation event2 = events.expectLogin().assertEvent(); List sessions = testUser.getUserSessions(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java index 87ec420695..cb40c1a7a0 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java @@ -171,12 +171,23 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); - String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent() + String authSessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setuptotp").assertEvent() .getDetails().get(Details.CODE_ID); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setuptotp").assertEvent() + .getDetails().get(Details.CODE_ID); + + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().user(userId).session(authSessionId1).detail(Details.USERNAME, "setuptotp").assertEvent(); } @Test @@ -380,17 +391,22 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { totpPage.configure(firstCode); - String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() + String authSessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() + .getDetails().get(Details.CODE_ID); + String authSessionId2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() .getDetails().get(Details.CODE_ID); + assertEquals(authSessionId1, authSessionId2); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent(); + EventRepresentation loginEvent = events.expectLogin().session(authSessionId1).assertEvent(); OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); - events.expectLogout(authSessionId).assertEvent(); + events.expectLogout(authSessionId1).assertEvent(); loginPage.open(); loginPage.login("test-user@localhost", "password"); @@ -448,7 +464,14 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { // After totp config, user should be on the app page assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_TOTP) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setuptotp2").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setuptotp2").assertEvent(); EventRepresentation loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent(); @@ -488,12 +511,21 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { totpPage.assertCurrent(); totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent() + String sessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setupTotp2").assertEvent() + .getDetails().get(Details.CODE_ID); + String sessionId2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(userId) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "setupTotp2").assertEvent() .getDetails().get(Details.CODE_ID); + assertEquals(sessionId1, sessionId2); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp2").assertEvent(); + events.expectLogin().user(userId).session(sessionId1).detail(Details.USERNAME, "setupTotp2").assertEvent(); } @Test @@ -520,12 +552,17 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { TimeBasedOTP timeBased = new TimeBasedOTP(HmacOTP.HMAC_SHA1, 8, 30, 1); totpPage.configure(timeBased.generateTOTP(totpSecret)); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() + String sessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() + .getDetails().get(Details.CODE_ID); + String sessionId2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() .getDetails().get(Details.CODE_ID); + assertEquals(sessionId1, sessionId2); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent(); + EventRepresentation loginEvent = events.expectLogin().session(sessionId1).assertEvent(); OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); @@ -575,12 +612,17 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { HmacOTP otpgen = new HmacOTP(6, HmacOTP.HMAC_SHA1, 1); totpPage.configure(otpgen.generateHOTP(totpSecret, 0)); String uri = driver.getCurrentUrl(); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() - .getDetails().get(Details.CODE_ID); + String sessionId1 = events.expectRequiredAction(EventType.UPDATE_TOTP) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() + .getDetails().get(Details.CODE_ID); + String sessionId2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent() + .getDetails().get(Details.CODE_ID); + assertEquals(sessionId1, sessionId2); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent(); + EventRepresentation loginEvent = events.expectLogin().session(sessionId1).assertEvent(); OAuthClient.AccessTokenResponse tokenResponse = sendTokenRequestAndGetResponse(loginEvent); oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); @@ -678,7 +720,14 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { .assertEvent(); } - EventRepresentation event2 = events.expectRequiredAction(EventType.UPDATE_TOTP).user(event1.getUserId()).detail(Details.USERNAME, "test-user@localhost").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_TOTP) + .user(event1.getUserId()) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "test-user@localhost").assertEvent(); + EventRepresentation event2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(event1.getUserId()) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .detail(Details.USERNAME, "test-user@localhost").assertEvent(); event2 = events.expectLogin().user(event2.getUserId()).session(event2.getDetails().get(Details.CODE_ID)).detail(Details.USERNAME, "test-user@localhost").assertEvent(); // assert old session is gone or is maintained diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java index d67ade3f68..a2ed7f3eeb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java @@ -43,6 +43,7 @@ import org.keycloak.federation.kerberos.KerberosConfig; import org.keycloak.federation.kerberos.KerberosFederationProviderFactory; import org.keycloak.models.Constants; import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.UserProfileAttributeMetadata; @@ -271,7 +272,9 @@ public class KerberosStandaloneTest extends AbstractKerberosSingleRealmTest { driver.navigate().to(changePasswordUrl.trim()); loginPasswordUpdatePage.assertCurrent(); loginPasswordUpdatePage.changePassword("resetPassword", "resetPassword"); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).client(oauth.getClientId()).detail(Details.USERNAME, "test-user@localhost"); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).client(oauth.getClientId()).detail(Details.USERNAME, "test-user@localhost"); + events.poll(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).client(oauth.getClientId()).detail(Details.USERNAME, "test-user@localhost"); infoPage.assertCurrent(); Assert.assertEquals("Your account has been updated.", infoPage.getInfo()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java index e847039cf9..3037f6bc5a 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java @@ -27,6 +27,7 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialModel; +import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.GroupModel; import org.keycloak.models.LDAPConstants; @@ -570,7 +571,8 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest { requiredActionChangePasswordPage.changePassword("Password1-updated2", "Password1-updated2"); appPage.assertCurrent(); - events.expect(EventType.UPDATE_PASSWORD).user(userId).assertEvent(); + events.expect(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).assertEvent(); + events.expect(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).assertEvent(); loginEvent = events.expectLogin().user(userId).assertEvent(); tokenResponse = sendTokenRequestAndGetResponse(loginEvent); appPage.logout(tokenResponse.getIdToken()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java index 02305fe6db..61855be4a5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java @@ -32,6 +32,7 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.UserModel; import org.keycloak.models.credential.OTPCredentialModel; @@ -175,7 +176,12 @@ public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest { appPage.assertCurrent(); // Logout - events.expect(EventType.UPDATE_TOTP).user(userRep.getId()).assertEvent(); //remove the UPDATE_TOTP event + events.expect(EventType.UPDATE_TOTP) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .user(userRep.getId()).assertEvent(); + events.expect(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE) + .user(userRep.getId()).assertEvent(); EventRepresentation loginEvent = events.expectLogin().user(userRep.getId()).assertEvent(); String idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken(); appPage.logout(idTokenHint); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java index 9110cf67ae..5fbb2f1c83 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java @@ -30,6 +30,7 @@ import org.keycloak.events.EventType; import org.keycloak.executors.ExecutorsProvider; import org.keycloak.models.Constants; import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -784,7 +785,8 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.updatePasswords("password", "password"); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).assertEvent(); userRepresentation = testRealm().users().get(userId).toRepresentation(); assertTrue(userRepresentation.isEnabled()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java index 1a9ba2bbc6..b25859253e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java @@ -754,7 +754,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { authenticateWithTotp(); totpSetupPage.assertCurrent(); totpSetupPage.configure(totp.generateTOTP(totpSetupPage.getTotpSecret()), "totp2-label"); - events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_TOTP).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); TokenCtx token2 = assertLoggedInWithAcr("gold"); // Trying to add another OTP by "kc_action". Level 2 should be required and user can choose between 2 OTP codes @@ -803,7 +804,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { authenticateWithTotp(); totpSetupPage.assertCurrent(); totpSetupPage.configure(totp.generateTOTP(totpSetupPage.getTotpSecret()), "totp2-label"); - events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_TOTP).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); TokenCtx token2 = assertLoggedInWithAcr("gold"); String otp2CredentialId = getCredentialIdByLabel("totp2-label"); @@ -818,7 +820,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { deleteCredentialPage.assertCredentialInMessage("totp2-label"); deleteCredentialPage.confirm(); - events.expectRequiredAction(EventType.REMOVE_TOTP).assertEvent(); + events.expectRequiredAction(EventType.REMOVE_TOTP).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.REMOVE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); assertLoggedInWithAcr("gold"); } @@ -841,7 +844,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { deleteCredentialPage.assertCurrent(); deleteCredentialPage.assertCredentialInMessage("otp"); deleteCredentialPage.confirm(); - events.expectRequiredAction(EventType.REMOVE_TOTP).assertEvent(); + events.expectRequiredAction(EventType.REMOVE_TOTP).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.REMOVE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); assertLoggedInWithAcr("gold"); // Trying to add OTP. No 2nd factor should be required as user doesn't have any @@ -850,7 +854,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { totpSetupPage.assertCurrent(); String totp2Secret = totpSetupPage.getTotpSecret(); totpSetupPage.configure(totp.generateTOTP(totp2Secret), "totp2-label"); - events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_TOTP).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, OTPCredentialModel.TYPE).assertEvent(); assertLoggedInWithAcr("silver"); // set time offset for OTP as it is not permitted to authenticate with same OTP code multiple times @@ -881,7 +886,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest { deleteCredentialPage.assertCurrent(); deleteCredentialPage.assertCredentialInMessage("Recovery codes"); deleteCredentialPage.confirm(); - events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).assertEvent(); + events.expectRequiredAction(EventType.REMOVE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, RecoveryAuthnCodesCredentialModel.TYPE).assertEvent(); assertLoggedInWithAcr("gold"); } finally { setOtpTimeOffset(0, totp); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index e31db36151..c7c6e1db20 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -44,6 +44,7 @@ import org.keycloak.models.BrowserSecurityHeaders; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.Constants; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; @@ -210,7 +211,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { //POST request to http://localhost:8180/auth/realms/test/protocol/openid-connect/auth; UriBuilder b = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(AUTH_SERVER_ROOT)); Response response = client.target(b.build(oauth.getRealm())).request().post(oauth.getLoginEntityForPOST()); - + assertThat(response.getStatus(), is(equalTo(200))); assertThat(response, Matchers.body(containsString("Sign in"))); @@ -531,7 +532,8 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { setTimeOffset(0); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).detail(Details.USERNAME, "login-test").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).detail(Details.USERNAME, "login-test").assertEvent(); String currentUrl = driver.getCurrentUrl(); String pageSource = driver.getPageSource(); @@ -710,14 +712,14 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { //login without remember me loginPage.setRememberMe(false); loginPage.login("login-test", "password"); - + // Expire session loginEvent = events.expectLogin().user(userId) .detail(Details.USERNAME, "login-test") .assertEvent(); sessionId = loginEvent.getSessionId(); testingClient.testing().removeUserSession("test", sessionId); - + // Assert rememberMe not checked nor username/email prefilled loginPage.open(); assertFalse(loginPage.isRememberMeChecked()); @@ -726,7 +728,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { setRememberMe(false); } } - + @Test // KEYCLOAK-3181 public void loginWithEmailUserAndRememberMe() { @@ -752,7 +754,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { // Assert rememberMe checked and username/email prefilled loginPage.open(); assertTrue(loginPage.isRememberMeChecked()); - + Assert.assertEquals("login@test.com", loginPage.getUsername()); loginPage.setRememberMe(false); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index b11b9711bd..70edaec9b5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -30,6 +30,7 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.Constants; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.SystemClientUtil; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; @@ -239,6 +240,18 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.changePassword("resetPassword", "resetPassword"); event = events.expectRequiredAction(EventType.UPDATE_PASSWORD) + .detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) + .client(expectedClientId) + .user(userId).detail(Details.USERNAME, username); + if (expectedRedirectUri != null) { + event.detail(Details.REDIRECT_URI, expectedRedirectUri); + } else { + event.removeDetail(Details.REDIRECT_URI); + } + event.assertEvent(); + + event = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) .client(expectedClientId) .user(userId).detail(Details.USERNAME, username); if (expectedRedirectUri != null) { @@ -414,7 +427,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.changePassword(password, password); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).detail(Details.USERNAME, username.trim()).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).detail(Details.USERNAME, username.trim()).assertEvent(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -465,7 +479,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.assertCurrent(); assertEquals(error, updatePasswordPage.getError()); - events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL_ERROR).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); } private void initiateResetPasswordFromResetPasswordPage(String username) { @@ -1040,11 +1055,13 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { assertEquals("Invalid password: minimum length 8.", resetPasswordPage.getErrorMessage()); - events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL_ERROR).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy"); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -1397,6 +1414,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { changePasswordOnUpdatePage(driver); events.expectRequiredAction(EventType.UPDATE_PASSWORD) + .detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) + .detail(Details.REDIRECT_URI, redirectUri) + .client(clientId) + .user(user.getId()).detail(Details.USERNAME, user.getUsername()).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE) .detail(Details.REDIRECT_URI, redirectUri) .client(clientId) .user(user.getId()).detail(Details.USERNAME, user.getUsername()).assertEvent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java index 2b309f183c..399de885d5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java @@ -26,6 +26,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -191,7 +192,8 @@ public class SSOTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.assertCurrent(); updatePasswordPage.changePassword("password", "password"); - events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnIdlessTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnIdlessTest.java index 4bc3829ddc..4ce0badd70 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnIdlessTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnIdlessTest.java @@ -218,13 +218,22 @@ public class WebAuthnIdlessTest extends AbstractWebAuthnVirtualTest { appPage.assertCurrent(); assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE)); - EventRepresentation eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION) + EventRepresentation eventRep1 = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION) .user(userId) .detail(Details.CUSTOM_REQUIRED_ACTION, raProviderID) .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel) .detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID) .assertEvent(); - String credentialId = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR); + EventRepresentation eventRep2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(userId) + .detail(Details.CUSTOM_REQUIRED_ACTION, raProviderID) + .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel) + .detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID) + .assertEvent(); + String credentialId1 = eventRep1.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR); + String credentialId2 = eventRep2.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR); + + assertThat(credentialId1, equalTo(credentialId2)); assertThat(userRes.credentials().stream() .filter(cred -> cred.getType().equals(credType)) @@ -250,7 +259,7 @@ public class WebAuthnIdlessTest extends AbstractWebAuthnVirtualTest { .user(userId) .client("account") .assertEvent(); - return credentialId; + return credentialId2; } protected void checkTryAnotherWay() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnRegisterAndLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnRegisterAndLoginTest.java index 611efd5af7..a9cb95a2a9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnRegisterAndLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/WebAuthnRegisterAndLoginTest.java @@ -31,6 +31,7 @@ import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterF import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; import org.keycloak.common.util.SecretGenerator; import org.keycloak.events.Details; +import org.keycloak.events.EventType; import org.keycloak.models.credential.WebAuthnCredentialModel; import org.keycloak.models.credential.dto.WebAuthnCredentialData; import org.keycloak.representations.idm.CredentialRepresentation; @@ -58,11 +59,11 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.keycloak.events.EventType.CUSTOM_REQUIRED_ACTION; import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE; import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED; @@ -127,13 +128,22 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest { // confirm that registration is successfully completed userId = events.expectRegister(username, email).assertEvent().getUserId(); // confirm registration event - EventRepresentation eventRep = events.expectRequiredAction(CUSTOM_REQUIRED_ACTION) + EventRepresentation eventRep1 = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION) .user(userId) .detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID) .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel) .detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID) .assertEvent(); - String regPubKeyCredentialId = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR); + EventRepresentation eventRep2 = events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(userId) + .detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID) + .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, authenticatorLabel) + .detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID) + .assertEvent(); + String regPubKeyCredentialId1 = eventRep1.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR); + String regPubKeyCredentialId2 = eventRep2.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR); + + assertThat(regPubKeyCredentialId1, equalTo(regPubKeyCredentialId2)); // confirm login event String sessionId = events.expectLogin() @@ -176,7 +186,7 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest { // confirm login event sessionId = events.expectLogin() .user(userId) - .detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, regPubKeyCredentialId) + .detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, regPubKeyCredentialId2) .detail(WebAuthnConstants.USER_VERIFICATION_CHECKED, Boolean.FALSE.toString()) .assertEvent().getSessionId(); @@ -235,7 +245,12 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest { webAuthnRegisterPage.assertCurrent(); - events.expectRequiredAction(CUSTOM_REQUIRED_ACTION) + events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION) + .user(userId) + .detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID) + .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL) + .assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) .user(userId) .detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID) .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL) @@ -246,7 +261,12 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest { appPage.assertCurrent(); - events.expectRequiredAction(CUSTOM_REQUIRED_ACTION) + events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION) + .user(userId) + .detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID) + .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL) + .assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) .user(userId) .detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID) .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/registration/WebAuthnOtherSettingsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/registration/WebAuthnOtherSettingsTest.java index 42d960a56f..d2851f1fcb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/registration/WebAuthnOtherSettingsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/registration/WebAuthnOtherSettingsTest.java @@ -86,6 +86,14 @@ public class WebAuthnOtherSettingsTest extends AbstractWebAuthnVirtualTest { .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "webauthn") .detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID) .assertEvent(); + events.expectRequiredAction(EventType.UPDATE_CREDENTIAL) + .user(userId) + .detail(Details.CUSTOM_REQUIRED_ACTION, isPasswordless() + ? WebAuthnPasswordlessRegisterFactory.PROVIDER_ID + : WebAuthnRegisterFactory.PROVIDER_ID) + .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "webauthn") + .detail(WebAuthnConstants.PUBKEY_CRED_AAGUID_ATTR, ALL_ZERO_AAGUID) + .assertEvent(); final String credentialType = getCredentialType(); // Soft token in Firefox does not increment counter diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/import/testrealm-keycloak-6146-error.json b/testsuite/integration-arquillian/tests/base/src/test/resources/import/testrealm-keycloak-6146-error.json index db16e081c0..15f2e5a30b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/import/testrealm-keycloak-6146-error.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/import/testrealm-keycloak-6146-error.json @@ -13,7 +13,7 @@ "failureFactor" : 3, "eventsEnabled" : true, "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "IMPERSONATE_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "RESTART_AUTHENTICATION", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "REGISTER", "LOGOUT", "CLIENT_REGISTER", "IDENTITY_PROVIDER_LINK_ACCOUNT", "UPDATE_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "IDENTITY_PROVIDER_FIRST_LOGIN", "VERIFY_EMAIL", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "RESTART_AUTHENTICATION_ERROR", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "EXECUTE_ACTION_TOKEN_ERROR", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "UPDATE_EMAIL", "REGISTER_ERROR", "REVOKE_GRANT_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "EXECUTE_ACTION_TOKEN", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "SEND_VERIFY_EMAIL_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "RESET_PASSWORD", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "SEND_RESET_PASSWORD_ERROR", "CLIENT_UPDATE", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" ], + "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "REMOVE_CREDENTIAL", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "IMPERSONATE_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "RESTART_AUTHENTICATION", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_CREDENTIAL_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "REGISTER", "LOGOUT", "CLIENT_REGISTER", "IDENTITY_PROVIDER_LINK_ACCOUNT", "UPDATE_CREDENTIAL", "UPDATE_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "IDENTITY_PROVIDER_FIRST_LOGIN", "VERIFY_EMAIL", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "RESTART_AUTHENTICATION_ERROR", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "EXECUTE_ACTION_TOKEN_ERROR", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "UPDATE_EMAIL", "REGISTER_ERROR", "REVOKE_GRANT_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "EXECUTE_ACTION_TOKEN", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "SEND_VERIFY_EMAIL_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "RESET_PASSWORD", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "REMOVE_CREDENTIAL_ERROR", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "SEND_RESET_PASSWORD_ERROR", "CLIENT_UPDATE", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" ], "adminEventsEnabled" : true, "adminEventsDetailsEnabled" : true, "requiredCredentials":[ diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/import/testrealm-keycloak-6146.json b/testsuite/integration-arquillian/tests/base/src/test/resources/import/testrealm-keycloak-6146.json index 30b87fa068..2891d8d6c7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/import/testrealm-keycloak-6146.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/import/testrealm-keycloak-6146.json @@ -14,7 +14,7 @@ "failureFactor" : 3, "eventsEnabled" : true, "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "UPDATE_TOTP", "REMOVE_TOTP", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "IMPERSONATE_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "RESTART_AUTHENTICATION", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "REGISTER", "LOGOUT", "CLIENT_REGISTER", "IDENTITY_PROVIDER_LINK_ACCOUNT", "UPDATE_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "IDENTITY_PROVIDER_FIRST_LOGIN", "VERIFY_EMAIL", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "RESTART_AUTHENTICATION_ERROR", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "EXECUTE_ACTION_TOKEN_ERROR", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "UPDATE_EMAIL", "REGISTER_ERROR", "REVOKE_GRANT_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "EXECUTE_ACTION_TOKEN", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "SEND_VERIFY_EMAIL_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "RESET_PASSWORD", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "SEND_RESET_PASSWORD_ERROR", "CLIENT_UPDATE", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" ], + "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "UPDATE_TOTP", "REMOVE_TOTP", "REMOVE_CREDENTIAL", "REVOKE_GRANT", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "IMPERSONATE_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "RESTART_AUTHENTICATION", "UPDATE_PROFILE_ERROR", "IMPERSONATE", "LOGIN", "UPDATE_PASSWORD_ERROR", "UPDATE_CREDENTIAL_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "REGISTER", "LOGOUT", "CLIENT_REGISTER", "IDENTITY_PROVIDER_LINK_ACCOUNT", "UPDATE_CREDENTIAL", "UPDATE_PASSWORD", "FEDERATED_IDENTITY_LINK_ERROR", "CLIENT_DELETE", "IDENTITY_PROVIDER_FIRST_LOGIN", "VERIFY_EMAIL", "CLIENT_DELETE_ERROR", "CLIENT_LOGIN_ERROR", "RESTART_AUTHENTICATION_ERROR", "REMOVE_FEDERATED_IDENTITY_ERROR", "EXECUTE_ACTIONS", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "EXECUTE_ACTION_TOKEN_ERROR", "SEND_VERIFY_EMAIL", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "UPDATE_EMAIL", "REGISTER_ERROR", "REVOKE_GRANT_ERROR", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "EXECUTE_ACTION_TOKEN", "CLIENT_UPDATE_ERROR", "UPDATE_PROFILE", "FEDERATED_IDENTITY_LINK", "CLIENT_REGISTER_ERROR", "SEND_VERIFY_EMAIL_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "RESET_PASSWORD", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "REMOVE_CREDENTIAL_ERROR", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "SEND_RESET_PASSWORD_ERROR", "CLIENT_UPDATE", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "CUSTOM_REQUIRED_ACTION_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" ], "adminEventsEnabled" : true, "adminEventsDetailsEnabled" : true, "requiredCredentials":[ diff --git a/themes/src/main/resources/theme/base/email/html/event-remove_credential.ftl b/themes/src/main/resources/theme/base/email/html/event-remove_credential.ftl new file mode 100644 index 0000000000..28e5650a48 --- /dev/null +++ b/themes/src/main/resources/theme/base/email/html/event-remove_credential.ftl @@ -0,0 +1,4 @@ +<#import "template.ftl" as layout> +<@layout.emailLayout> +${kcSanitize(msg("eventRemoveCredentialBodyHtml", event.details.credential_type!"unknown", event.date, event.ipAddress))?no_esc} + diff --git a/themes/src/main/resources/theme/base/email/html/event-update_credential.ftl b/themes/src/main/resources/theme/base/email/html/event-update_credential.ftl new file mode 100644 index 0000000000..61aec8d658 --- /dev/null +++ b/themes/src/main/resources/theme/base/email/html/event-update_credential.ftl @@ -0,0 +1,4 @@ +<#import "template.ftl" as layout> +<@layout.emailLayout> +${kcSanitize(msg("eventUpdateCredentialBodyHtml", event.details.credential_type!"unknown", event.date, event.ipAddress))?no_esc} + diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties index 4f41282efa..740d86c508 100755 --- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties @@ -33,6 +33,12 @@ eventUpdatePasswordBodyHtml=

Your password was changed on {0} from {1}. If thi eventUpdateTotpSubject=Update OTP eventUpdateTotpBody=OTP was updated for your account on {0} from {1}. If this was not you, please contact an administrator. eventUpdateTotpBodyHtml=

OTP was updated for your account on {0} from {1}. If this was not you, please contact an administrator.

+eventUpdateCredentialSubject=Update credential +eventUpdateCredentialBody=Your {0} credential was changed on {1} from {2}. If this was not you, please contact an administrator. +eventUpdateCredentialBodyHtml=

Your {0} credential was changed on {1} from {2}. If this was not you, please contact an administrator.

+eventRemoveCredentialSubject=Remove credential +eventRemoveCredentialBody=Credential {0} was removed from your account on {1} from {2}. If this was not you, please contact an administrator. +eventRemoveCredentialBodyHtml=

Credential {0} was removed from your account on {1} from {2}. If this was not you, please contact an administrator.

requiredAction.CONFIGURE_TOTP=Configure OTP requiredAction.TERMS_AND_CONDITIONS=Terms and Conditions diff --git a/themes/src/main/resources/theme/base/email/text/event-remove_credential.ftl b/themes/src/main/resources/theme/base/email/text/event-remove_credential.ftl new file mode 100644 index 0000000000..7e4c26d127 --- /dev/null +++ b/themes/src/main/resources/theme/base/email/text/event-remove_credential.ftl @@ -0,0 +1,2 @@ +<#ftl output_format="plainText"> +${msg("eventRemoveCredentialBody", event.details.credential_type!"unknown", event.date, event.ipAddress)} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/text/event-update_credential.ftl b/themes/src/main/resources/theme/base/email/text/event-update_credential.ftl new file mode 100644 index 0000000000..95f0d05296 --- /dev/null +++ b/themes/src/main/resources/theme/base/email/text/event-update_credential.ftl @@ -0,0 +1,2 @@ +<#ftl output_format="plainText"> +${msg("eventUpdateCredentialBody", event.details.credential_type!"unknown", event.date, event.ipAddress)} \ No newline at end of file