[KEYCLOAK-5629] Add credential endpoints to account service
This commit is contained in:
parent
5aebc74f8c
commit
f022bc1269
5 changed files with 196 additions and 4 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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 : "");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue