Skip to content

Commit

Permalink
Merge pull request #16 from neondatabase/delete-user
Browse files Browse the repository at this point in the history
Add an endpoint to self-delete user account
  • Loading branch information
kszafran authored Aug 7, 2024
2 parents 766e01b + d2bb623 commit 95fbe20
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 44 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ Documentation on how to create Keycloak plugins can be found in the

## Build

Ensure you have the Maven build command `mvn` installed. An IDE such as Intellij

Ensure you have the Maven build command `mvn` installed. An IDE such as Intellij
may provide this for you automatically

Run the following command:
Expand Down Expand Up @@ -46,5 +45,4 @@ you must include the compiled JAR file in the release artifacts, as dependent
projects are expected to download this directly from GitHub as part of their
build pipeline


[1]: https://www.keycloak.org/docs/latest/server_development/index.html#_providers
73 changes: 48 additions & 25 deletions src/main/java/account_update/AccountChangeResourceProvider.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package account_update;

import account_update.email_update.NeonUpdateEmailActionToken;
import jakarta.mail.internet.AddressException;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import account_update.email_update.NeonUpdateEmailActionToken;
import org.jboss.logging.Logger;

import org.keycloak.authorization.util.Tokens;
import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException;
Expand All @@ -24,26 +26,25 @@
import org.keycloak.userprofile.UserProfileContext;

import java.util.concurrent.TimeUnit;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

public class AccountChangeResourceProvider implements RealmResourceProvider {

private KeycloakSession session;
private Auth auth;
private RealmModel realm;
private EventBuilder event;
private ClientModel client;
private static Logger logger = Logger.getLogger(AccountChangeResourceProvider.class);
private static int Timeout = 60 * 15;
private static final Logger LOG = Logger.getLogger(AccountChangeResourceProvider.class);

private static final int TIMEOUT = 60 * 15;

private final KeycloakSession session;
private final Auth auth;
private final RealmModel realm;
private final EventBuilder event;

public AccountChangeResourceProvider(KeycloakSession session) {
this.session = session;
this.realm = session.getContext().getRealm();

this.client = this.realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
ClientModel client = this.realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
if (client == null || !client.isEnabled()) {
logger.debug("account management not enabled");
LOG.debug("account management not enabled");
throw new NotFoundException("account management not enabled");
}
AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)
Expand All @@ -58,20 +59,15 @@ public AccountChangeResourceProvider(KeycloakSession session) {
this.event = new EventBuilder(realm, session, session.getContext().getConnection());
}

@Context
private SecurityContext securityContext;

@Override
public Object getResource() {
return this;
}

@Override
public void close() {

}


@PUT
@Path("/update-user-email/{clientId}")
@Produces(MediaType.APPLICATION_JSON)
Expand All @@ -88,26 +84,24 @@ public Response updateUserEmail(@PathParam("clientId") String clientId, String n
}

NeonUpdateEmailActionToken actionToken = new NeonUpdateEmailActionToken(user.getId(),
Time.currentTime() + Timeout,
Time.currentTime() + TIMEOUT,
user.getEmail(), newEmail, clientId, true);

UriInfo uriInfo = session.getContext().getUri();
String link = Urls
.actionTokenBuilder(uriInfo.getBaseUri(), actionToken.serialize(session, realm, uriInfo),
clientId, "")
.actionTokenBuilder(uriInfo.getBaseUri(), actionToken.serialize(session, realm, uriInfo), clientId, "")
.build(realm.getName()).toString();

try {
session.getProvider(EmailTemplateProvider.class).setRealm(realm)
.setUser(user).sendEmailUpdateConfirmation(link, TimeUnit.SECONDS.toMinutes(Timeout), newEmail);
.setUser(user).sendEmailUpdateConfirmation(link, TimeUnit.SECONDS.toMinutes(TIMEOUT), newEmail);
} catch (EmailException e) {
if (e.getCause() instanceof AddressException) {
return Response.status(Response.Status.BAD_REQUEST).
entity("Bad address given for email - " + e.getCause().getMessage()).build();
}


logger.error("Failed to send email for email update", e);
LOG.error("Failed to send email for email update", e);
event.event(EventType.UPDATE_EMAIL_ERROR).error(Errors.EMAIL_SEND_FAILED);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
Expand All @@ -133,14 +127,43 @@ public Response updateUserPassword(String newPassword) {
try {
user.credentialManager().updateCredential(UserCredentialModel.password(newPassword, false));
} catch (Exception e) {
logger.error("Failed to update user password", e);
LOG.error("Failed to update user password", e);
event.event(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}

return Response.ok().entity("Password updated successfully").build();
}

@DELETE
@Path("/delete-user-account")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response deleteUserAccount() {
auth.require(AccountRoles.DELETE_ACCOUNT);
event.event(EventType.DELETE_ACCOUNT).detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name());

UserModel userFromToken = getUserFromToken(session);

UserModel user = session.users().getUserById(realm, userFromToken.getId());
if (user == null) {
return Response.status(Response.Status.NOT_FOUND).entity("User not found").build();
}

try {
boolean removed = new UserManager(session).removeUser(realm, user);
if (!removed) {
throw new RuntimeException("User was not removed");
}
} catch (Exception e) {
LOG.error("Failed to delete user account", e);
event.event(EventType.DELETE_ACCOUNT_ERROR).error(Errors.USER_DELETE_ERROR);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}

return Response.ok().entity("Account deleted successfully").build();
}

private UserModel getUserFromToken(KeycloakSession keycloakSession) {
AccessToken accessToken = Tokens.getAccessToken(keycloakSession);
if (accessToken.getSessionState() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,18 @@ public RealmResourceProvider create(KeycloakSession keycloakSession) {

@Override
public void init(Config.Scope scope) {

}

@Override
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {

}

@Override
public void close() {

}

@Override
public String getId() {
return ID;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
// modified from UpdateEmailActionTokenHandler
// https://github.com/keycloak/keycloak/blob/66f0d2ff1db6f5ec442b0ddab4580bdd652d8877/services/src/main/java/org/keycloak/authentication/actiontoken/updateemail/UpdateEmailActionTokenHandler.java
public class NeonUpdateEmailActionTokenHandler extends AbstractActionTokenHandler<NeonUpdateEmailActionToken> {

private final String connectionString;

public NeonUpdateEmailActionTokenHandler() {
Expand Down
11 changes: 6 additions & 5 deletions src/main/java/trust_email/Authenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ public class Authenticator implements org.keycloak.authentication.Authenticator
private static final Logger LOG = Logger.getLogger(Authenticator.class);

@Override
public void close() {}
public void close() {
}

@Override
public void action(AuthenticationFlowContext context) {}
public void action(AuthenticationFlowContext context) {
}

@Override
public void authenticate(AuthenticationFlowContext context) {
Expand All @@ -25,7 +27,7 @@ public void authenticate(AuthenticationFlowContext context) {

@Override
public boolean configuredFor(KeycloakSession session, RealmModel model, UserModel user) {
return false;
return false;
}

@Override
Expand All @@ -47,7 +49,6 @@ public void setRequiredActions(KeycloakSession session, RealmModel model, UserMo
manager.getStoredCredentialsByTypeStream(PasswordCredentialModel.TYPE).forEach(c -> manager.removeStoredCredentialById(c.getId()));

user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);

user.setEmailVerified(true);
}
}
}
13 changes: 7 additions & 6 deletions src/main/java/trust_email/AuthenticatorFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;


public class AuthenticatorFactory implements org.keycloak.authentication.AuthenticatorFactory {

@Override
Expand All @@ -18,13 +17,16 @@ public org.keycloak.authentication.Authenticator create(KeycloakSession session)
}

@Override
public void init(Scope config) {}
public void init(Scope config) {
}

@Override
public void postInit(KeycloakSessionFactory factory) {}
public void postInit(KeycloakSessionFactory factory) {
}

@Override
public void close() {}
public void close() {
}

@Override
public String getId() {
Expand Down Expand Up @@ -53,7 +55,7 @@ public Requirement[] getRequirementChoices() {

@Override
public boolean isUserSetupAllowed() {
return true;
return true;
}

@Override
Expand All @@ -65,5 +67,4 @@ public String getHelpText() {
public List<ProviderConfigProperty> getConfigProperties() {
return new ArrayList<>();
}

}

0 comments on commit 95fbe20

Please sign in to comment.