keycloak-uncached

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 : "");
     }