Details
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
new file mode 100644
index 0000000..42cf4e4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountCredentialResource.java
@@ -0,0 +1,118 @@
+package org.keycloak.services.resources.account;
+
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.credential.CredentialProvider;
+import org.keycloak.credential.PasswordCredentialProvider;
+import org.keycloak.credential.PasswordCredentialProviderFactory;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.ErrorResponse;
+import org.keycloak.utils.MediaType;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+public class AccountCredentialResource {
+
+ private final KeycloakSession session;
+ private final EventBuilder event;
+ private final UserModel user;
+ private final RealmModel realm;
+
+ public AccountCredentialResource(KeycloakSession session, EventBuilder event, UserModel user) {
+ this.session = session;
+ this.event = event;
+ this.user = user;
+ realm = session.getContext().getRealm();
+ }
+
+ @GET
+ @Path("password")
+ @Produces(MediaType.APPLICATION_JSON)
+ public PasswordDetails passwordDetails() {
+ PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) session.getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
+ CredentialModel password = passwordProvider.getPassword(realm, user);
+
+ PasswordDetails details = new PasswordDetails();
+ if (password != null) {
+ details.setRegistered(true);
+ details.setLastUpdate(password.getCreatedDate());
+ } else {
+ details.setRegistered(false);
+ }
+
+ return details;
+ }
+
+ @POST
+ @Path("password")
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response passwordUpdate(PasswordUpdate update) {
+ event.event(EventType.UPDATE_PASSWORD);
+
+ UserCredentialModel cred = UserCredentialModel.password(update.getCurrentPassword());
+ if (!session.userCredentialManager().isValid(realm, user, cred)) {
+ event.error(org.keycloak.events.Errors.INVALID_USER_CREDENTIALS);
+ return ErrorResponse.error(Errors.INVALID_CREDENTIALS, Response.Status.BAD_REQUEST);
+ }
+
+ session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(update.getNewPassword(), false));
+
+ return Response.ok().build();
+ }
+
+ public static class PasswordDetails {
+
+ private boolean registered;
+ private long lastUpdate;
+
+ public boolean isRegistered() {
+ return registered;
+ }
+
+ public void setRegistered(boolean registered) {
+ this.registered = registered;
+ }
+
+ public long getLastUpdate() {
+ return lastUpdate;
+ }
+
+ public void setLastUpdate(long lastUpdate) {
+ this.lastUpdate = lastUpdate;
+ }
+
+ }
+
+ public static class PasswordUpdate {
+
+ private String currentPassword;
+ private String newPassword;
+
+ public String getCurrentPassword() {
+ return currentPassword;
+ }
+
+ public void setCurrentPassword(String currentPassword) {
+ this.currentPassword = currentPassword;
+ }
+
+ public String getNewPassword() {
+ return newPassword;
+ }
+
+ public void setNewPassword(String newPassword) {
+ this.newPassword = newPassword;
+ }
+
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
index e327519..32d88de 100755
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
@@ -263,6 +263,11 @@ public class AccountRestService {
return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build();
}
+ @Path("/credentials")
+ public AccountCredentialResource credentials() {
+ return new AccountCredentialResource(session, event, user);
+ }
+
// TODO Federated identities
// TODO Applications
// TODO Logs
diff --git a/services/src/main/java/org/keycloak/services/resources/account/Errors.java b/services/src/main/java/org/keycloak/services/resources/account/Errors.java
index 6cb55bd..ad890b1 100644
--- a/services/src/main/java/org/keycloak/services/resources/account/Errors.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/Errors.java
@@ -25,5 +25,6 @@ public class Errors {
public static final String EMAIL_EXISTS = "email_exists";
public static final String READ_ONLY_USER = "user_read_only";
public static final String READ_ONLY_USERNAME = "username_read_only";
+ public static final String INVALID_CREDENTIALS = "invalid_credentials";
}
diff --git a/services/src/main/java/org/keycloak/services/resources/account/PasswordUtil.java b/services/src/main/java/org/keycloak/services/resources/account/PasswordUtil.java
new file mode 100644
index 0000000..a178a1b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/account/PasswordUtil.java
@@ -0,0 +1,26 @@
+package org.keycloak.services.resources.account;
+
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+public class PasswordUtil {
+
+ private KeycloakSession session;
+ private UserModel user;
+
+ public PasswordUtil(KeycloakSession session, UserModel user) {
+ this.session = session;
+ this.user = user;
+ }
+
+ public boolean isConfigured(KeycloakSession session, RealmModel realm, UserModel user) {
+ return session.userCredentialManager().isConfiguredFor(realm, user, CredentialModel.PASSWORD);
+ }
+
+ public void update() {
+
+ }
+
+}
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 47ed56f..6dbade1 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
@@ -28,20 +28,19 @@ import org.keycloak.representations.account.SessionRepresentation;
import org.keycloak.representations.account.UserRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.resources.account.AccountCredentialResource;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.TokenUtil;
import org.keycloak.testsuite.util.UserBuilder;
+import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -192,6 +191,49 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
assertEquals(1, sessions.size());
}
+ @Test
+ public void testGetPasswordDetails() throws IOException {
+ getPasswordDetails();
+ }
+
+ @Test
+ public void testPostPasswordUpdate() throws IOException {
+ //Get the time of lastUpdate
+ AccountCredentialResource.PasswordDetails initialDetails = getPasswordDetails();
+
+ //Change the password
+ updatePassword("password", "Str0ng3rP4ssw0rd", 200);
+
+ //Get the new value for lastUpdate
+ AccountCredentialResource.PasswordDetails updatedDetails = getPasswordDetails();
+ assertTrue(initialDetails.getLastUpdate() < updatedDetails.getLastUpdate());
+
+ //Try to change password again; should fail as current password is incorrect
+ updatePassword("password", "Str0ng3rP4ssw0rd", 400);
+
+ //Verify that lastUpdate hasn't changed
+ AccountCredentialResource.PasswordDetails finalDetails = getPasswordDetails();
+ assertEquals(updatedDetails.getLastUpdate(), finalDetails.getLastUpdate());
+
+ //Change the password back
+ updatePassword("Str0ng3rP4ssw0rd", "password", 200);
+ }
+
+ private AccountCredentialResource.PasswordDetails getPasswordDetails() throws IOException {
+ AccountCredentialResource.PasswordDetails details = SimpleHttp.doGet(getAccountUrl("credentials/password"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<AccountCredentialResource.PasswordDetails>() {});
+ assertTrue(details.isRegistered());
+ assertNotNull(details.getLastUpdate());
+ return details;
+ }
+
+ private void updatePassword(String currentPass, String newPass, int expectedStatus) throws IOException {
+ AccountCredentialResource.PasswordUpdate passwordUpdate = new AccountCredentialResource.PasswordUpdate();
+ passwordUpdate.setCurrentPassword(currentPass);
+ passwordUpdate.setNewPassword(newPass);
+ int status = SimpleHttp.doPost(getAccountUrl("credentials/password"), client).auth(tokenUtil.getToken()).json(passwordUpdate).asStatus();
+ assertEquals(expectedStatus, status);
+ }
+
private String getAccountUrl(String resource) {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : "");
}