diff --git a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/UserEntity.java b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/UserEntity.java
index f60d9d395fc91e982f5b556b804def8258547a78..5075f5cc15e89ae7575742219ef7ef230a013c01 100644
--- a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/UserEntity.java
+++ b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/UserEntity.java
@@ -102,6 +102,12 @@ public class UserEntity extends AbstractBaseEntity {
 	@Column(name = "last_failed_update")
 	private Date lastFailedUpdate;
 	
+	@Column(name = "expires_warn_sent_at")
+	private Date expireWarningSent;
+
+	@Column(name = "expired_sent_at")
+	private Date expiredSent;
+	
 	@Column(name = "theme", length = 128)
 	private String theme;
 	
@@ -307,5 +313,21 @@ public class UserEntity extends AbstractBaseEntity {
 
 	public void setName(String name) {
 		this.name = name;
+	}
+
+	public Date getExpireWarningSent() {
+		return expireWarningSent;
+	}
+
+	public void setExpireWarningSent(Date expireWarningSent) {
+		this.expireWarningSent = expireWarningSent;
+	}
+
+	public Date getExpiredSent() {
+		return expiredSent;
+	}
+
+	public void setExpiredSent(Date expiredSent) {
+		this.expiredSent = expiredSent;
 	}	
 }
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/job/UserExpire.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/job/UserExpire.java
new file mode 100644
index 0000000000000000000000000000000000000000..a8708c383fe1a9dbc89ad33df1120f0e129dfeb3
--- /dev/null
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/job/UserExpire.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Michael Simon.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ * 
+ * Contributors:
+ *     Michael Simon - initial
+ ******************************************************************************/
+package edu.kit.scc.webreg.job;
+
+import java.util.List;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.service.UserService;
+
+
+public class UserExpire extends AbstractExecutableJob {
+
+	private static final long serialVersionUID = 1L;
+
+	@Override
+	public void execute() {
+		Logger logger = LoggerFactory.getLogger(UserExpire.class);
+
+		Integer limit, days;
+		String emailTemplateName = "user_expiry";
+
+		if (getJobStore().containsKey("user_expiry_template")) {
+			emailTemplateName = getJobStore().get("user_expiry_template");
+		}
+		
+		if (getJobStore().containsKey("limit")) {
+			limit = Integer.parseInt(getJobStore().get("limit"));
+		}
+		else {
+			limit = 1;
+		}
+
+		if (getJobStore().containsKey("days")) {
+			days = Integer.parseInt(getJobStore().get("days"));
+		}
+		else {
+			days = 14;
+		}
+		
+		try {
+			InitialContext ic = new InitialContext();
+			
+			UserService service = (UserService) ic.lookup("global/bwreg/bwreg-service/UserServiceImpl!edu.kit.scc.webreg.service.UserService");
+			List<UserEntity> userList = service.findUsersForExpiry(limit, days);
+			
+			logger.debug("Found {} users suitable for expiry", userList.size());
+			
+			for (UserEntity user : userList) {
+				logger.debug("Inspecting user {} - {} - {} - {} - {}", user.getId(), user.getEppn(), user.getEmail(), user.getUserStatus(), user.getLastStatusChange());
+				service.expireUser(user, emailTemplateName);
+			}
+			
+		} catch (NamingException e) {
+			logger.warn("Could not expire users: {}", e);
+		}
+	}
+}
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/job/UserExpireSendWarning.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/job/UserExpireSendWarning.java
new file mode 100644
index 0000000000000000000000000000000000000000..d1c2d76f5234ddb7a182dfecfd7a46137b0c7184
--- /dev/null
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/job/UserExpireSendWarning.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Michael Simon.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ * 
+ * Contributors:
+ *     Michael Simon - initial
+ ******************************************************************************/
+package edu.kit.scc.webreg.job;
+
+import java.util.List;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.service.UserService;
+
+
+public class UserExpireSendWarning extends AbstractExecutableJob {
+
+	private static final long serialVersionUID = 1L;
+
+	@Override
+	public void execute() {
+		Logger logger = LoggerFactory.getLogger(UserExpireSendWarning.class);
+
+		Integer limit, days;
+		String emailTemplateName = "user_expiry_warning";
+
+		if (getJobStore().containsKey("user_expiry_warning_template")) {
+			emailTemplateName = getJobStore().get("user_expiry_warning_template");
+		}
+		
+		if (getJobStore().containsKey("limit")) {
+			limit = Integer.parseInt(getJobStore().get("limit"));
+		}
+		else {
+			limit = 1;
+		}
+
+		if (getJobStore().containsKey("days")) {
+			days = Integer.parseInt(getJobStore().get("days"));
+		}
+		else {
+			days = 90;
+		}
+		
+		try {
+			InitialContext ic = new InitialContext();
+			
+			UserService service = (UserService) ic.lookup("global/bwreg/bwreg-service/UserServiceImpl!edu.kit.scc.webreg.service.UserService");
+			List<UserEntity> userList = service.findUsersForExpiryWarning(limit, days);
+			
+			logger.debug("Found {} users suitable for sending expire warning", userList.size());
+			
+			for (UserEntity user : userList) {
+				logger.debug("Inspecting user {} - {} - {} - {} - {}", user.getId(), user.getEppn(), user.getEmail(), user.getUserStatus(), user.getLastStatusChange());
+				service.sendUserExpiryWarning(user, emailTemplateName);
+			}
+			
+		} catch (NamingException e) {
+			logger.warn("Could not send expire warning to user: {}", e);
+		}
+	}
+}
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/UserService.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/UserService.java
index 8db0a548270421a57cb487a35b301f1961bb2329..94b7ab30382d82077f9f60c5614986bb54d73e45 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/UserService.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/UserService.java
@@ -57,4 +57,12 @@ public interface UserService extends BaseService<UserEntity> {
 
 	SamlUserEntity findByPersistent(String spId, String idpId, String persistentId);
 
+	List<UserEntity> findUsersForExpiryWarning(int limit, int days);
+
+	void sendUserExpiryWarning(UserEntity user, String emailTemplateName);
+
+	List<UserEntity> findUsersForExpiry(int limit, int daysSinceWarning);
+
+	void expireUser(UserEntity user, String emailTemplateName);
+
 }
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserCreateServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserCreateServiceImpl.java
index 98b70a99b6db0d5635f9a5361caadd66e2e5b66f..9bf83262a087b150eea818d3f7d6809a88265bdb 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserCreateServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserCreateServiceImpl.java
@@ -51,7 +51,7 @@ import edu.kit.scc.webreg.event.exc.EventSubmitException;
 import edu.kit.scc.webreg.exc.UserUpdateException;
 import edu.kit.scc.webreg.service.SamlIdpMetadataService;
 import edu.kit.scc.webreg.service.UserCreateService;
-import edu.kit.scc.webreg.service.group.HomeOrgGroupUpdater;
+import edu.kit.scc.webreg.service.group.SamlGroupUpdater;
 import edu.kit.scc.webreg.service.identity.IdentityCreater;
 import edu.kit.scc.webreg.service.saml.Saml2AssertionService;
 import edu.kit.scc.webreg.service.saml.SamlIdentifier;
@@ -78,10 +78,10 @@ public class UserCreateServiceImpl implements UserCreateService {
 	private SamlUserDao samlUserDao;
 
 	@Inject
-	private UserUpdater userUpdater;
+	private SamlUserUpdater userUpdater;
 
 	@Inject
-	private HomeOrgGroupUpdater homeOrgGroupUpdater;
+	private SamlGroupUpdater homeOrgGroupUpdater;
 
 	@Inject
 	private RoleDao roleDao;
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserLoginServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserLoginServiceImpl.java
index 0fce336ef8b5fcce9af102b97820ae522eaca2c1..7c61d32790c9aa142baa980facf7d2a3e579f0c4 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserLoginServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserLoginServiceImpl.java
@@ -113,7 +113,7 @@ public class UserLoginServiceImpl implements UserLoginService, Serializable {
 	private SamlUserDao samlUserDao;
 
 	@Inject
-	private UserUpdater userUpdater;
+	private SamlUserUpdater userUpdater;
 
 	@Inject
 	private KnowledgeSessionSingleton knowledgeSessionService;
@@ -751,7 +751,7 @@ public class UserLoginServiceImpl implements UserLoginService, Serializable {
 				} else {
 					logger.info("Performing user update for {} with id {}",
 							new Object[] { user.getEppn(), user.getId() });
-					user = userUpdater.updateUserFromIdp(user, service, executor, null);
+					user = userUpdater.updateUserFromHomeOrg(user, service, executor, null);
 				}
 			} catch (UserUpdateException e) {
 				logger.warn("Could not update user (attrq is optional, continue with login process) {}: {}",
@@ -771,7 +771,7 @@ public class UserLoginServiceImpl implements UserLoginService, Serializable {
 					logger.info("Performing user update for {} with id {}",
 							new Object[] { user.getEppn(), user.getId() });
 
-					user = userUpdater.updateUserFromIdp(user, service, executor, null);
+					user = userUpdater.updateUserFromHomeOrg(user, service, executor, null);
 				}
 			} catch (UserUpdateException e) {
 				logger.warn("Could not update user {}: {} (attrq is mandatory, denying access)", e.getMessage(),
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserServiceImpl.java
index 70b29aea6ff4ed3695dbd51dca4010f886bdce93..9de0dfc285d6612b8bb7945b59dc661609f9fe93 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserServiceImpl.java
@@ -13,9 +13,14 @@ package edu.kit.scc.webreg.service.impl;
 import static edu.kit.scc.webreg.dao.ops.PaginateBy.withLimit;
 import static edu.kit.scc.webreg.dao.ops.RqlExpressions.and;
 import static edu.kit.scc.webreg.dao.ops.RqlExpressions.equal;
+import static edu.kit.scc.webreg.dao.ops.RqlExpressions.isNotNull;
+import static edu.kit.scc.webreg.dao.ops.RqlExpressions.isNull;
+import static edu.kit.scc.webreg.dao.ops.RqlExpressions.lessThan;
 import static edu.kit.scc.webreg.dao.ops.RqlExpressions.lessThanOrEqualTo;
 import static edu.kit.scc.webreg.dao.ops.SortBy.ascendingBy;
+import static java.time.temporal.ChronoUnit.DAYS;
 
+import java.time.Instant;
 import java.util.Date;
 import java.util.List;
 
@@ -28,6 +33,7 @@ import edu.kit.scc.webreg.dao.SamlUserDao;
 import edu.kit.scc.webreg.dao.UserDao;
 import edu.kit.scc.webreg.dao.UserLoginInfoDao;
 import edu.kit.scc.webreg.dao.identity.IdentityDao;
+import edu.kit.scc.webreg.dao.ops.SortBy;
 import edu.kit.scc.webreg.entity.GroupEntity;
 import edu.kit.scc.webreg.entity.RegistryEntity;
 import edu.kit.scc.webreg.entity.RegistryStatus;
@@ -41,6 +47,7 @@ import edu.kit.scc.webreg.entity.UserStatus;
 import edu.kit.scc.webreg.entity.identity.IdentityEntity;
 import edu.kit.scc.webreg.exc.UserUpdateException;
 import edu.kit.scc.webreg.service.UserService;
+import edu.kit.scc.webreg.service.user.UserLifecycleManager;
 import edu.kit.scc.webreg.session.HttpRequestContext;
 import jakarta.ejb.Stateless;
 import jakarta.inject.Inject;
@@ -56,11 +63,14 @@ public class UserServiceImpl extends BaseServiceImpl<UserEntity> implements User
 	@Inject
 	private UserDao dao;
 
+	@Inject
+	private UserLifecycleManager userLifecycleManager;
+
 	@Inject
 	private SamlUserDao samlUserDao;
 
 	@Inject
-	private UserUpdater userUpdater;
+	private SamlUserUpdater userUpdater;
 
 	@Inject
 	private RegistryDao registryDao;
@@ -103,6 +113,39 @@ public class UserServiceImpl extends BaseServiceImpl<UserEntity> implements User
 		return loginInfo;
 	}
 
+	@Override
+	public List<UserEntity> findUsersForExpiryWarning(int limit, int days) {
+		Date dateBeforeNDays = Date.from(Instant.now().minus(days, DAYS));
+		// Find all users, where 
+		// expireWarningSent is null: No expiry warning sent until now
+		// lastUpdate was before the specified days
+		// scheduledUpdate is not null: External API users have no scheduled upadte. Only update users, that 
+		// have update schedule set
+		return dao.findAll(withLimit(limit), SortBy.ascendingBy(UserEntity_.lastUpdate),
+				and(equal(UserEntity_.userStatus, UserStatus.ACTIVE), isNull(UserEntity_.expireWarningSent),
+						lessThan(UserEntity_.lastUpdate, dateBeforeNDays), isNotNull(UserEntity_.scheduledUpdate)));
+	}
+
+	@Override
+	public List<UserEntity> findUsersForExpiry(int limit, int daysSinceWarning) {
+		Date dateBeforeNDays = Date.from(Instant.now().minus(daysSinceWarning, DAYS));
+		return dao.findAll(withLimit(limit), SortBy.ascendingBy(UserEntity_.expireWarningSent),
+				and(equal(UserEntity_.userStatus, UserStatus.ACTIVE), isNotNull(UserEntity_.expireWarningSent),
+						lessThan(UserEntity_.expireWarningSent, dateBeforeNDays), isNotNull(UserEntity_.scheduledUpdate)));
+	}
+
+	@Override
+	public void sendUserExpiryWarning(UserEntity user, String emailTemplateName) {
+		user = dao.fetch(user.getId());
+		userLifecycleManager.sendUserExpiryWarning(user, emailTemplateName);
+	}
+
+	@Override
+	public void expireUser(UserEntity user, String emailTemplateName) {
+		user = dao.fetch(user.getId());
+		userLifecycleManager.expireUser(user, emailTemplateName);
+	}
+
 	@Override
 	public SamlUserEntity findByPersistent(String spId, String idpId, String persistentId) {
 		return samlUserDao.findByPersistent(spId, idpId, persistentId);
@@ -154,7 +197,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserEntity> implements User
 	public SamlUserEntity updateUserFromIdp(SamlUserEntity user, String executor, StringBuffer debugLog)
 			throws UserUpdateException {
 		user = samlUserDao.fetch(user.getId());
-		return userUpdater.updateUserFromIdp(user, null, executor, debugLog);
+		return userUpdater.updateUserFromHomeOrg(user, null, executor, debugLog);
 	}
 
 	@Override
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdateFromHomeOrgServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdateFromHomeOrgServiceImpl.java
index b29800244ff93ad26065dd5a68716722d13a2049..f4447873157ef2e7a0bf0ff103028c61448b7455 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdateFromHomeOrgServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdateFromHomeOrgServiceImpl.java
@@ -26,7 +26,6 @@ import edu.kit.scc.webreg.entity.UserStatus;
 import edu.kit.scc.webreg.entity.oidc.OidcUserEntity;
 import edu.kit.scc.webreg.exc.UserUpdateException;
 import edu.kit.scc.webreg.service.UserUpdateFromHomeOrgService;
-import edu.kit.scc.webreg.service.oidc.client.OidcUserUpdater;
 
 @Stateless
 public class UserUpdateFromHomeOrgServiceImpl implements UserUpdateFromHomeOrgService, Serializable {
@@ -40,7 +39,7 @@ public class UserUpdateFromHomeOrgServiceImpl implements UserUpdateFromHomeOrgSe
 	private UserDao userDao;
 
 	@Inject
-	private UserUpdater userUpdater;
+	private SamlUserUpdater userUpdater;
 
 	@Inject
 	private OidcUserUpdater oidcUserUpdate;
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdateServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdateServiceImpl.java
index 17273094b9f2837621c94f44e80be963c3b0881a..48d47fefbb5ed0caa066dea953ceccad5489cfe5 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdateServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdateServiceImpl.java
@@ -52,7 +52,7 @@ public class UserUpdateServiceImpl implements UserUpdateService, Serializable {
 	private UserDao userDao;
 
 	@Inject
-	private UserUpdater userUpdater;
+	private SamlUserUpdater userUpdater;
 
 	@Inject
 	private KnowledgeSessionSingleton knowledgeSessionService;
@@ -276,7 +276,7 @@ public class UserUpdateServiceImpl implements UserUpdateService, Serializable {
 
 					// TODO check for OIDC user entity (refresh token?)
 					if (user instanceof SamlUserEntity)
-						user = userUpdater.updateUserFromIdp((SamlUserEntity) user, service, executor, null);
+						user = userUpdater.updateUserFromHomeOrg((SamlUserEntity) user, service, executor, null);
 				}
 			} catch (UserUpdateException e) {
 				logger.warn("Could not update user (attrq is optional, continue with login process) {}: {}",
@@ -298,7 +298,7 @@ public class UserUpdateServiceImpl implements UserUpdateService, Serializable {
 
 					// TODO check for OIDC user entity (refresh token?)
 					if (user instanceof SamlUserEntity)
-						user = userUpdater.updateUserFromIdp((SamlUserEntity) user, service, executor, null);
+						user = userUpdater.updateUserFromHomeOrg((SamlUserEntity) user, service, executor, null);
 				}
 			} catch (UserUpdateException e) {
 				logger.warn("Could not update user {}: {}", e.getMessage(), user.getEppn());
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthClientCallbackServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthClientCallbackServiceImpl.java
index 533c6550b8b3031b778a6b56ff5b01539ee16220..8aa58c851b818c44f2e3e21814c544c8c0312cac 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthClientCallbackServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthClientCallbackServiceImpl.java
@@ -51,6 +51,7 @@ import edu.kit.scc.webreg.entity.oauth.OAuthRpFlowStateEntity_;
 import edu.kit.scc.webreg.entity.oauth.OAuthUserEntity;
 import edu.kit.scc.webreg.entity.oauth.OAuthUserEntity_;
 import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.service.impl.OAuthUserUpdater;
 import edu.kit.scc.webreg.service.saml.exc.OidcAuthenticationException;
 import edu.kit.scc.webreg.session.SessionManager;
 import jakarta.ejb.Stateless;
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthUserCreateService.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthUserCreateService.java
index 6a09c921b14735fbdec952fe234d5c0a4d97e0cb..9b2641dd64d52aa8983e30b40cd4ffdc06f7eafc 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthUserCreateService.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthUserCreateService.java
@@ -40,8 +40,10 @@ import edu.kit.scc.webreg.event.EventSubmitter;
 import edu.kit.scc.webreg.event.UserEvent;
 import edu.kit.scc.webreg.event.exc.EventSubmitException;
 import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.service.group.OAuthGroupUpdater;
 import edu.kit.scc.webreg.service.identity.IdentityCreater;
 import edu.kit.scc.webreg.service.impl.AttributeMapHelper;
+import edu.kit.scc.webreg.service.impl.OAuthUserUpdater;
 import edu.kit.scc.webreg.session.HttpRequestContext;
 import jakarta.ejb.Stateless;
 import jakarta.ejb.TransactionManagement;
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthUserUpdater.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthUserUpdater.java
deleted file mode 100644
index 9edf61dc4ab7bba6e0076240622b9a7aedda42f5..0000000000000000000000000000000000000000
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthUserUpdater.java
+++ /dev/null
@@ -1,473 +0,0 @@
-package edu.kit.scc.webreg.service.oauth.client;
-
-import java.lang.reflect.InvocationTargetException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Random;
-import java.util.Set;
-
-import org.apache.commons.beanutils.PropertyUtils;
-import org.slf4j.Logger;
-import org.slf4j.MDC;
-
-import edu.kit.scc.webreg.audit.Auditor;
-import edu.kit.scc.webreg.audit.RegistryAuditor;
-import edu.kit.scc.webreg.audit.UserUpdateAuditor;
-import edu.kit.scc.webreg.bootstrap.ApplicationConfig;
-import edu.kit.scc.webreg.dao.RegistryDao;
-import edu.kit.scc.webreg.dao.SerialDao;
-import edu.kit.scc.webreg.dao.as.ASUserAttrDao;
-import edu.kit.scc.webreg.dao.audit.AuditDetailDao;
-import edu.kit.scc.webreg.dao.audit.AuditEntryDao;
-import edu.kit.scc.webreg.dao.jpa.oauth.OAuthUserDao;
-import edu.kit.scc.webreg.entity.EventType;
-import edu.kit.scc.webreg.entity.GroupEntity;
-import edu.kit.scc.webreg.entity.RegistryEntity;
-import edu.kit.scc.webreg.entity.RegistryStatus;
-import edu.kit.scc.webreg.entity.ServiceEntity;
-import edu.kit.scc.webreg.entity.ServiceEntity_;
-import edu.kit.scc.webreg.entity.UserEntity;
-import edu.kit.scc.webreg.entity.UserStatus;
-import edu.kit.scc.webreg.entity.as.ASUserAttrEntity;
-import edu.kit.scc.webreg.entity.as.AttributeSourceServiceEntity;
-import edu.kit.scc.webreg.entity.attribute.IncomingAttributeSetEntity;
-import edu.kit.scc.webreg.entity.audit.AuditStatus;
-import edu.kit.scc.webreg.entity.oauth.OAuthUserEntity;
-import edu.kit.scc.webreg.event.EventSubmitter;
-import edu.kit.scc.webreg.event.UserEvent;
-import edu.kit.scc.webreg.event.exc.EventSubmitException;
-import edu.kit.scc.webreg.exc.RegisterException;
-import edu.kit.scc.webreg.exc.UserUpdateException;
-import edu.kit.scc.webreg.hook.HookManager;
-import edu.kit.scc.webreg.hook.UserServiceHook;
-import edu.kit.scc.webreg.service.ServiceService;
-import edu.kit.scc.webreg.service.attribute.IncomingOAuthAttributesHandler;
-import edu.kit.scc.webreg.service.identity.IdentityUpdater;
-import edu.kit.scc.webreg.service.impl.AbstractUserUpdater;
-import edu.kit.scc.webreg.service.impl.AttributeMapHelper;
-import edu.kit.scc.webreg.service.reg.AttributeSourceQueryService;
-import edu.kit.scc.webreg.service.reg.impl.Registrator;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-
-@ApplicationScoped
-public class OAuthUserUpdater extends AbstractUserUpdater<OAuthUserEntity> {
-
-	private static final long serialVersionUID = 1L;
-
-	@Inject
-	private Logger logger;
-
-	@Inject
-	private AuditEntryDao auditDao;
-
-	@Inject
-	private AuditDetailDao auditDetailDao;
-
-	@Inject
-	private OAuthUserDao userDao;
-
-	@Inject
-	private ServiceService serviceService;
-
-	@Inject
-	private RegistryDao registryDao;
-
-	@Inject
-	private SerialDao serialDao;
-
-	@Inject
-	private HookManager hookManager;
-
-	@Inject
-	private OAuthGroupUpdater oauthGroupUpdater;
-
-	@Inject
-	private ASUserAttrDao asUserAttrDao;
-
-	@Inject
-	private AttributeSourceQueryService attributeSourceQueryService;
-
-	@Inject
-	private EventSubmitter eventSubmitter;
-
-	@Inject
-	private ApplicationConfig appConfig;
-
-	@Inject
-	private Registrator registrator;
-
-	@Inject
-	private AttributeMapHelper attrHelper;
-
-	@Inject
-	private IdentityUpdater identityUpdater;
-
-	@Inject
-	private IncomingOAuthAttributesHandler incomingAttributeHandler;
-	
-	public OAuthUserEntity updateUserFromOP(OAuthUserEntity user, String executor, StringBuffer debugLog)
-			throws UserUpdateException {
-		throw new UserUpdateException("Not implemented");
-	}
-
-	@Override
-	public OAuthUserEntity updateUser(OAuthUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		return updateUser(user, attributeMap, executor, null, null, lastLoginHost);
-	}
-
-	@Override
-	public OAuthUserEntity updateUser(OAuthUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			ServiceEntity service, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		MDC.put("userId", "" + user.getId());
-		logger.debug("Updating OIDC user {}", user.getEppn());
-
-		boolean changed = false;
-
-		UserUpdateAuditor auditor = new UserUpdateAuditor(auditDao, auditDetailDao, appConfig);
-		auditor.startAuditTrail(executor);
-		auditor.setName(getClass().getName() + "-UserUpdate-Audit");
-		auditor.setDetail("Update OAuth user " + user.getOauthId());
-
-		changed |= preUpdateUser(user, attributeMap, user.getOauthIssuer().getGenericStore(), executor, service, debugLog);
-
-		// List to store parent services, that are not registered. Need to be registered
-		// later, when attribute map is populated
-		List<ServiceEntity> delayedRegisterList = new ArrayList<ServiceEntity>();
-
-		/**
-		 * put no_assertion_count in generic store if assertion is missing. Else reset
-		 * no assertion count and put last valid assertion date in
-		 */
-		if (attributeMap == null) {
-			if (!user.getGenericStore().containsKey("no_assertion_count")) {
-				user.getGenericStore().put("no_assertion_count", "1");
-			} else {
-				user.getGenericStore().put("no_assertion_count",
-						"" + (Long.parseLong(user.getGenericStore().get("no_assertion_count")) + 1L));
-			}
-
-			logger.info("No attribute for user {}, skipping updateFromAttribute", user.getEppn());
-
-			user.getAttributeStore().clear();
-
-			if (UserStatus.ACTIVE.equals(user.getUserStatus())) {
-				changeUserStatus(user, UserStatus.ON_HOLD, auditor);
-
-				/*
-				 * Also flag all registries for user ON_HOLD
-				 */
-				List<RegistryEntity> registryList = registryDao.findByUserAndStatus(user, RegistryStatus.ACTIVE,
-						RegistryStatus.LOST_ACCESS, RegistryStatus.INVALID);
-				for (RegistryEntity registry : registryList) {
-					changeRegistryStatus(registry, RegistryStatus.ON_HOLD, "user-on-hold", auditor);
-				}
-			}
-		} else {
-			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-			user.getGenericStore().put("no_assertion_count", "0");
-			user.getGenericStore().put("last_valid_assertion", df.format(new Date()));
-
-			changed |= updateUserFromAttribute(user, attributeMap, auditor);
-
-			if (UserStatus.ON_HOLD.equals(user.getUserStatus())) {
-				changeUserStatus(user, UserStatus.ACTIVE, auditor);
-
-				/*
-				 * Also reenable all registries for user to LOST_ACCESS. They are rechecked then
-				 */
-				List<RegistryEntity> registryList = registryDao.findByUserAndStatus(user, RegistryStatus.ON_HOLD);
-				for (RegistryEntity registry : registryList) {
-					changeRegistryStatus(registry, RegistryStatus.LOST_ACCESS, "user-reactivated", auditor);
-
-					/*
-					 * check if parent registry is missing
-					 */
-					if (registry.getService().getParentService() != null) {
-						List<RegistryEntity> parentRegistryList = registryDao.findByServiceAndIdentityAndNotStatus(
-								registry.getService().getParentService(), user.getIdentity(), RegistryStatus.DELETED,
-								RegistryStatus.DEPROVISIONED);
-						if (parentRegistryList.size() == 0) {
-							delayedRegisterList.add(registry.getService().getParentService());
-						}
-					}
-				}
-
-				/*
-				 * fire a user changed event to be sure, when the user is activated
-				 */
-				changed = true;
-			}
-
-			/*
-			 * if service is set, update only attribute sources spcific for this service.
-			 * Else update all (login via web or generic attribute query)
-			 */
-			if (service != null) {
-				service = serviceService.findByIdWithAttrs(service.getId(), ServiceEntity_.attributeSourceService);
-
-				for (AttributeSourceServiceEntity asse : service.getAttributeSourceService()) {
-					changed |= attributeSourceQueryService.updateUserAttributes(user, asse.getAttributeSource(),
-							executor);
-				}
-			} else {
-				List<ASUserAttrEntity> asUserAttrList = asUserAttrDao.findForUser(user);
-				for (ASUserAttrEntity asUserAttr : asUserAttrList) {
-					changed |= attributeSourceQueryService.updateUserAttributes(user, asUserAttr.getAttributeSource(),
-							executor);
-				}
-			}
-
-			Set<GroupEntity> changedGroups = oauthGroupUpdater.updateGroupsForUser(user, attributeMap, auditor);
-
-			if (changedGroups.size() > 0) {
-				changed = true;
-			}
-
-			Map<String, String> attributeStore = user.getAttributeStore();
-			for (Entry<String, List<Object>> entry : attributeMap.entrySet()) {
-				attributeStore.put(entry.getKey(), attrHelper.attributeListToString(entry.getValue()));
-			}
-
-			IncomingAttributeSetEntity incomingAttributeSet = incomingAttributeHandler.createOrUpdateAttributes(user, attributeMap);
-			incomingAttributeHandler.processIncomingAttributeSet(incomingAttributeSet);
-
-			identityUpdater.updateIdentity(user);
-			
-			if (appConfig.getConfigValue("create_missing_eppn_scope") != null) {
-				if (user.getEppn() == null) {
-					String scope = appConfig.getConfigValue("create_missing_eppn_scope");
-					user.setEppn(user.getIdentity().getGeneratedLocalUsername() + "@" + scope);
-					changed = true;
-				}
-			}
-		}
-
-		for (ServiceEntity delayedService : delayedRegisterList) {
-			try {
-				registrator.registerUser(user, delayedService, "user-" + user.getId(), false);
-			} catch (RegisterException e) {
-				logger.warn("Parent registrytion didn't work out like it should", e);
-			}
-		}
-
-		changed |= postUpdateUser(user, attributeMap, user.getOauthIssuer().getGenericStore(), executor, service, debugLog,
-				lastLoginHost);
-
-		user.setLastUpdate(new Date());
-		user.setLastFailedUpdate(null);
-		user.setScheduledUpdate(getNextScheduledUpdate());
-
-		if (changed) {
-			fireUserChangeEvent(user, auditor.getActualExecutor(), auditor);
-		}
-
-		auditor.setUser(user);
-		auditor.finishAuditTrail();
-		auditor.commitAuditTrail();
-
-		return user;
-	}
-
-//	public OAuthUserEntity updateUser(OAuthUserEntity user, IDTokenClaimsSet claims, UserInfo userInfo,
-//			RefreshToken refreshToken, BearerAccessToken bat, String executor, ServiceEntity service,
-//			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-//
-//		Map<String, List<Object>> attributeMap = oidcTokenHelper.convertToAttributeMap(claims, userInfo, refreshToken,
-//				bat);
-//
-//		if (service != null)
-//			return updateUser(user, attributeMap, executor, service, debugLog, lastLoginHost);
-//		else
-//			return updateUser(user, attributeMap, executor, debugLog, lastLoginHost);
-//	}
-
-//	public OAuthUserEntity updateUser(OAuthUserEntity user, IDTokenClaimsSet claims, UserInfo userInfo,
-//			RefreshToken refreshToken, BearerAccessToken bat, String executor, StringBuffer debugLog,
-//			String lastLoginHost) throws UserUpdateException {
-//
-//		return updateUser(user, claims, userInfo, refreshToken, bat, executor, null, debugLog, lastLoginHost);
-//	}
-
-	protected void fireUserChangeEvent(UserEntity user, String executor, Auditor auditor) {
-
-		UserEvent userEvent = new UserEvent(user, auditor.getAudit());
-
-		try {
-			eventSubmitter.submit(userEvent, EventType.USER_UPDATE, executor);
-		} catch (EventSubmitException e) {
-			logger.warn("Could not submit event", e);
-		}
-	}
-
-	public boolean updateUserNew(OAuthUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			Auditor auditor, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		boolean changed = false;
-
-		changed |= preUpdateUser(user, attributeMap, user.getOauthIssuer().getGenericStore(), executor, null, debugLog);
-		changed |= updateUserFromAttribute(user, attributeMap, auditor);
-		changed |= postUpdateUser(user, attributeMap, user.getOauthIssuer().getGenericStore(), executor, null, debugLog,
-				lastLoginHost);
-
-		return changed;
-	}
-
-	public boolean updateUserFromAttribute(UserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
-			throws UserUpdateException {
-		return updateUserFromAttribute(user, attributeMap, false, auditor);
-	}
-
-	public boolean updateUserFromAttribute(UserEntity user, Map<String, List<Object>> attributeMap,
-			boolean withoutUidNumber, Auditor auditor) throws UserUpdateException {
-
-		boolean changed = false;
-
-		UserServiceHook completeOverrideHook = null;
-		Set<UserServiceHook> activeHooks = new HashSet<UserServiceHook>();
-
-		for (UserServiceHook hook : hookManager.getUserHooks()) {
-			if (hook.isResponsible(user, attributeMap)) {
-
-				hook.preUpdateUserFromAttribute(user, attributeMap, auditor);
-				activeHooks.add(hook);
-
-				if (hook.isCompleteOverride()) {
-					completeOverrideHook = hook;
-				}
-			}
-		}
-
-		if (completeOverrideHook == null) {
-			@SuppressWarnings("unchecked")
-			HashMap<String, Object> userMap = (HashMap<String, Object>) attributeMap.get("user").get(0);
-
-			if (userMap.get("email") != null && (userMap.get("email") instanceof String))
-				changed |= compareAndChangeProperty(user, "email", (String) userMap.get("email"), auditor);
-			else
-				changed |= compareAndChangeProperty(user, "email", null, auditor);
-
-			if (userMap.get("name") != null && (userMap.get("name") instanceof String))
-				changed |= compareAndChangeProperty(user, "name", (String) userMap.get("name"), auditor);
-			else
-				changed |= compareAndChangeProperty(user, "name", null, auditor);
-
-			if ((!withoutUidNumber) && (user.getUidNumber() == null)) {
-				user.setUidNumber(serialDao.nextUidNumber().intValue());
-				logger.info("Setting UID Number {} for user {}", user.getUidNumber(), user.getEppn());
-				auditor.logAction(user.getEppn(), "SET FIELD", "uidNumber", "" + user.getUidNumber(),
-						AuditStatus.SUCCESS);
-				changed = true;
-			}
-		} else {
-			logger.info("Overriding standard User Update Mechanism! Activator: {}",
-					completeOverrideHook.getClass().getName());
-		}
-
-		for (UserServiceHook hook : activeHooks) {
-			hook.postUpdateUserFromAttribute(user, attributeMap, auditor);
-		}
-
-		return changed;
-	}
-
-	private boolean compareAndChangeProperty(UserEntity user, String property, String value, Auditor auditor) {
-		String s = null;
-		String action = null;
-
-		try {
-			Object actualValue = PropertyUtils.getProperty(user, property);
-
-			if (actualValue != null && actualValue.equals(value)) {
-				// Value didn't change, do nothing
-				return false;
-			}
-
-			if (actualValue == null && value == null) {
-				// Value stayed null
-				return false;
-			}
-
-			if (actualValue == null) {
-				s = "null";
-				action = "SET FIELD";
-			} else {
-				s = actualValue.toString();
-				action = "UPDATE FIELD";
-			}
-
-			s = s + " -> " + value;
-			if (s.length() > 1017)
-				s = s.substring(0, 1017) + "...";
-
-			PropertyUtils.setProperty(user, property, value);
-
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.SUCCESS);
-		} catch (IllegalAccessException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		} catch (InvocationTargetException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		} catch (NoSuchMethodException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		}
-
-		return true;
-	}
-
-	protected void changeUserStatus(UserEntity user, UserStatus toStatus, Auditor auditor) {
-		UserStatus fromStatus = user.getUserStatus();
-		user.setUserStatus(toStatus);
-		user.setLastStatusChange(new Date());
-
-		logger.debug("{}: change user status from {} to {}", user.getEppn(), fromStatus, toStatus);
-		auditor.logAction(user.getEppn(), "CHANGE STATUS", fromStatus + " -> " + toStatus,
-				"Change status " + fromStatus + " -> " + toStatus, AuditStatus.SUCCESS);
-	}
-
-	protected void changeRegistryStatus(RegistryEntity registry, RegistryStatus toStatus, String statusMessage,
-			Auditor parentAuditor) {
-		RegistryStatus fromStatus = registry.getRegistryStatus();
-		registry.setRegistryStatus(toStatus);
-		registry.setStatusMessage(statusMessage);
-		registry.setLastStatusChange(new Date());
-
-		logger.debug("{} {} {}: change registry status from {} to {}", new Object[] { registry.getUser().getEppn(),
-				registry.getService().getShortName(), registry.getId(), fromStatus, toStatus });
-		RegistryAuditor registryAuditor = new RegistryAuditor(auditDao, auditDetailDao, appConfig);
-		registryAuditor.setParent(parentAuditor);
-		registryAuditor.startAuditTrail(parentAuditor.getActualExecutor());
-		registryAuditor.setName(getClass().getName() + "-UserUpdate-Registry-Audit");
-		registryAuditor.setDetail("Update registry " + registry.getId() + " for user " + registry.getUser().getEppn());
-		registryAuditor.setRegistry(registry);
-		registryAuditor.logAction(registry.getUser().getEppn(), "CHANGE STATUS", "registry-" + registry.getId(),
-				"Change status " + fromStatus + " -> " + toStatus, AuditStatus.SUCCESS);
-		registryAuditor.finishAuditTrail();
-	}
-
-	private Date getNextScheduledUpdate() {
-		Long futureMillis = 30L * 24L * 60L * 60L * 1000L;
-		if (appConfig.getConfigOptions().containsKey("update_schedule_future")) {
-			futureMillis = Long.decode(appConfig.getConfigValue("update_schedule_future"));
-		}
-		Integer futureMillisRandom = 6 * 60 * 60 * 1000;
-		if (appConfig.getConfigOptions().containsKey("update_schedule_future_random")) {
-			futureMillisRandom = Integer.decode(appConfig.getConfigValue("update_schedule_future_random"));
-		}
-		Random r = new Random();
-		return new Date(System.currentTimeMillis() + futureMillis + r.nextInt(futureMillisRandom));
-	}
-
-	protected void updateFail(OAuthUserEntity user) {
-		user.setLastFailedUpdate(new Date());
-		user.setScheduledUpdate(getNextScheduledUpdate());
-	}
-}
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcClientCallbackServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcClientCallbackServiceImpl.java
index f94222b2c3afb2b5ba8af9d06eaa6b2179112308..4f99eaa5169bddce6f41eb8590976b5a3743d245 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcClientCallbackServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcClientCallbackServiceImpl.java
@@ -42,6 +42,7 @@ import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
 import com.nimbusds.openid.connect.sdk.claims.UserInfo;
 import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
 
+import edu.kit.scc.regapp.oidc.tools.OidcOpMetadataSingletonBean;
 import edu.kit.scc.regapp.oidc.tools.OidcTokenHelper;
 import edu.kit.scc.webreg.annotations.RetryTransaction;
 import edu.kit.scc.webreg.dao.UserLoginInfoDao;
@@ -56,6 +57,7 @@ import edu.kit.scc.webreg.entity.oidc.OidcRpFlowStateEntity_;
 import edu.kit.scc.webreg.entity.oidc.OidcUserEntity;
 import edu.kit.scc.webreg.entity.oidc.OidcUserEntity_;
 import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.service.impl.OidcUserUpdater;
 import edu.kit.scc.webreg.service.saml.exc.OidcAuthenticationException;
 import edu.kit.scc.webreg.session.SessionManager;
 import jakarta.ejb.Stateless;
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcClientRedirectServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcClientRedirectServiceImpl.java
index 151aaf1fa6fd307fc8af22455f7030bb7ef179bf..09bce84ff15c2c54a66a7054c58fddf3d0f043f2 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcClientRedirectServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcClientRedirectServiceImpl.java
@@ -22,6 +22,7 @@ import com.nimbusds.oauth2.sdk.id.State;
 import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
 import com.nimbusds.openid.connect.sdk.Nonce;
 
+import edu.kit.scc.regapp.oidc.tools.OidcOpMetadataSingletonBean;
 import edu.kit.scc.webreg.annotations.RetryTransaction;
 import edu.kit.scc.webreg.dao.oidc.OidcRpConfigurationDao;
 import edu.kit.scc.webreg.dao.oidc.OidcRpFlowStateDao;
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserCreateServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserCreateServiceImpl.java
index 2031fd5f5b3fb2e017d550e2f97d9ae22361a8e6..65faf7fe4db30ecb06bc9da3c0728cbf2216ca56 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserCreateServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserCreateServiceImpl.java
@@ -44,8 +44,10 @@ import edu.kit.scc.webreg.event.EventSubmitter;
 import edu.kit.scc.webreg.event.UserEvent;
 import edu.kit.scc.webreg.event.exc.EventSubmitException;
 import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.service.group.OidcGroupUpdater;
 import edu.kit.scc.webreg.service.identity.IdentityCreater;
 import edu.kit.scc.webreg.service.impl.AttributeMapHelper;
+import edu.kit.scc.webreg.service.impl.OidcUserUpdater;
 import edu.kit.scc.webreg.session.HttpRequestContext;
 import jakarta.ejb.Stateless;
 import jakarta.ejb.TransactionManagement;
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserServiceImpl.java
index 98090b540a03612e3c520975f1403fe214e371a3..e7a753f38bb0aa55a3fb9e3d456bd5d24c7b8fd2 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserServiceImpl.java
@@ -20,6 +20,7 @@ import edu.kit.scc.webreg.dao.oidc.OidcUserDao;
 import edu.kit.scc.webreg.entity.oidc.OidcUserEntity;
 import edu.kit.scc.webreg.exc.UserUpdateException;
 import edu.kit.scc.webreg.service.impl.BaseServiceImpl;
+import edu.kit.scc.webreg.service.impl.OidcUserUpdater;
 
 @Stateless
 public class OidcUserServiceImpl extends BaseServiceImpl<OidcUserEntity> implements OidcUserService, Serializable {
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserUpdater.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserUpdater.java
deleted file mode 100644
index 70265d2b7ccba72be5065de0ad90a6941cfd7c75..0000000000000000000000000000000000000000
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcUserUpdater.java
+++ /dev/null
@@ -1,605 +0,0 @@
-package edu.kit.scc.webreg.service.oidc.client;
-
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Random;
-import java.util.Set;
-
-import org.apache.commons.beanutils.PropertyUtils;
-import org.slf4j.Logger;
-import org.slf4j.MDC;
-
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.proc.BadJOSEException;
-import com.nimbusds.jwt.JWT;
-import com.nimbusds.jwt.JWTParser;
-import com.nimbusds.oauth2.sdk.AuthorizationGrant;
-import com.nimbusds.oauth2.sdk.ErrorObject;
-import com.nimbusds.oauth2.sdk.ParseException;
-import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
-import com.nimbusds.oauth2.sdk.TokenErrorResponse;
-import com.nimbusds.oauth2.sdk.TokenRequest;
-import com.nimbusds.oauth2.sdk.TokenResponse;
-import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
-import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
-import com.nimbusds.oauth2.sdk.auth.Secret;
-import com.nimbusds.oauth2.sdk.http.HTTPResponse;
-import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.oauth2.sdk.id.Issuer;
-import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
-import com.nimbusds.oauth2.sdk.token.RefreshToken;
-import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
-import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
-import com.nimbusds.openid.connect.sdk.UserInfoRequest;
-import com.nimbusds.openid.connect.sdk.UserInfoResponse;
-import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
-import com.nimbusds.openid.connect.sdk.claims.UserInfo;
-import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
-
-import edu.kit.scc.regapp.oidc.tools.OidcTokenHelper;
-import edu.kit.scc.webreg.audit.Auditor;
-import edu.kit.scc.webreg.audit.RegistryAuditor;
-import edu.kit.scc.webreg.audit.UserUpdateAuditor;
-import edu.kit.scc.webreg.bootstrap.ApplicationConfig;
-import edu.kit.scc.webreg.dao.RegistryDao;
-import edu.kit.scc.webreg.dao.SerialDao;
-import edu.kit.scc.webreg.dao.as.ASUserAttrDao;
-import edu.kit.scc.webreg.dao.audit.AuditDetailDao;
-import edu.kit.scc.webreg.dao.audit.AuditEntryDao;
-import edu.kit.scc.webreg.dao.oidc.OidcUserDao;
-import edu.kit.scc.webreg.entity.EventType;
-import edu.kit.scc.webreg.entity.GroupEntity;
-import edu.kit.scc.webreg.entity.RegistryEntity;
-import edu.kit.scc.webreg.entity.RegistryStatus;
-import edu.kit.scc.webreg.entity.ServiceEntity;
-import edu.kit.scc.webreg.entity.ServiceEntity_;
-import edu.kit.scc.webreg.entity.UserEntity;
-import edu.kit.scc.webreg.entity.UserStatus;
-import edu.kit.scc.webreg.entity.as.ASUserAttrEntity;
-import edu.kit.scc.webreg.entity.as.AttributeSourceServiceEntity;
-import edu.kit.scc.webreg.entity.attribute.IncomingAttributeSetEntity;
-import edu.kit.scc.webreg.entity.audit.AuditStatus;
-import edu.kit.scc.webreg.entity.oidc.OidcRpConfigurationEntity;
-import edu.kit.scc.webreg.entity.oidc.OidcUserEntity;
-import edu.kit.scc.webreg.event.EventSubmitter;
-import edu.kit.scc.webreg.event.UserEvent;
-import edu.kit.scc.webreg.event.exc.EventSubmitException;
-import edu.kit.scc.webreg.exc.RegisterException;
-import edu.kit.scc.webreg.exc.UserUpdateException;
-import edu.kit.scc.webreg.hook.HookManager;
-import edu.kit.scc.webreg.hook.UserServiceHook;
-import edu.kit.scc.webreg.service.ServiceService;
-import edu.kit.scc.webreg.service.attribute.IncomingOidcAttributesHandler;
-import edu.kit.scc.webreg.service.identity.IdentityUpdater;
-import edu.kit.scc.webreg.service.impl.AbstractUserUpdater;
-import edu.kit.scc.webreg.service.impl.AttributeMapHelper;
-import edu.kit.scc.webreg.service.reg.AttributeSourceQueryService;
-import edu.kit.scc.webreg.service.reg.impl.Registrator;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-
-@ApplicationScoped
-public class OidcUserUpdater extends AbstractUserUpdater<OidcUserEntity> {
-
-	private static final long serialVersionUID = 1L;
-
-	@Inject
-	private Logger logger;
-
-	@Inject
-	private AuditEntryDao auditDao;
-
-	@Inject
-	private AuditDetailDao auditDetailDao;
-
-	@Inject
-	private OidcUserDao userDao;
-
-	@Inject
-	private ServiceService serviceService;
-
-	@Inject
-	private RegistryDao registryDao;
-
-	@Inject
-	private SerialDao serialDao;
-
-	@Inject
-	private HookManager hookManager;
-
-	@Inject
-	private OidcGroupUpdater oidcGroupUpdater;
-
-	@Inject
-	private ASUserAttrDao asUserAttrDao;
-
-	@Inject
-	private AttributeSourceQueryService attributeSourceQueryService;
-
-	@Inject
-	private EventSubmitter eventSubmitter;
-
-	@Inject
-	private ApplicationConfig appConfig;
-
-	@Inject
-	private Registrator registrator;
-
-	@Inject
-	private AttributeMapHelper attrHelper;
-
-	@Inject
-	private OidcTokenHelper oidcTokenHelper;
-
-	@Inject
-	private OidcOpMetadataSingletonBean opMetadataBean;
-
-	@Inject
-	private IdentityUpdater identityUpdater;
-
-	@Inject
-	private IncomingOidcAttributesHandler incomingAttributeHandler;
-	
-	public OidcUserEntity updateUserFromOP(OidcUserEntity user, String executor, StringBuffer debugLog)
-			throws UserUpdateException {
-
-		try {
-			/**
-			 * TODO Implement refresh here
-			 */
-			OidcRpConfigurationEntity rpConfig = user.getIssuer();
-
-			if (user.getAttributeStore().get("refreshToken") == null) {
-				updateFail(user);
-				throw new UserUpdateException("refresh token is null");
-			}
-			
-			RefreshToken token = new RefreshToken(user.getAttributeStore().get("refreshToken"));
-			AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(token);
-
-			ClientID clientID = new ClientID(user.getIssuer().getClientId());
-			Secret clientSecret = new Secret(user.getIssuer().getSecret());
-			ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret);
-
-			TokenRequest tokenRequest = new TokenRequest(opMetadataBean.getTokenEndpointURI(user.getIssuer()),
-					clientAuth, refreshTokenGrant);
-			TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send());
-
-			if (!tokenResponse.indicatesSuccess()) {
-				TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
-				ErrorObject error = errorResponse.getErrorObject();
-				logger.info("Got error: code {}, desc {}, http-status {}, uri {}", error.getCode(),
-						error.getDescription());
-				updateFail(user);
-			} else {
-				OIDCTokenResponse oidcTokenResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();
-				logger.debug("response: {}", oidcTokenResponse.toJSONObject());
-
-				JWT idToken = oidcTokenResponse.getOIDCTokens().getIDToken();
-				IDTokenClaimsSet claims = null;
-
-				if (idToken != null) {
-					IDTokenValidator validator = new IDTokenValidator(new Issuer(rpConfig.getServiceUrl()),
-							new ClientID(rpConfig.getClientId()), JWSAlgorithm.RS256,
-							opMetadataBean.getJWKSetURI(rpConfig).toURL());
-
-					try {
-						claims = validator.validate(idToken, null);
-						logger.debug("Got signed claims verified from {}: {}", claims.getIssuer(), claims.getSubject());
-					} catch (BadJOSEException | JOSEException e) {
-						throw new UserUpdateException("signature failed: " + e.getMessage());
-					}
-				}
-
-				RefreshToken refreshToken = null;
-
-				if (oidcTokenResponse.getOIDCTokens().getRefreshToken() != null) {
-
-					refreshToken = oidcTokenResponse.getOIDCTokens().getRefreshToken();
-					try {
-						JWT refreshJwt = JWTParser.parse(refreshToken.getValue());
-						// Well, what to do with this info? Check if refresh token is short or long
-						// lived? <1 day?
-						logger.info("refresh will expire at: {}", refreshJwt.getJWTClaimsSet().getExpirationTime());
-					} catch (java.text.ParseException e) {
-						logger.debug("Refresh token is no JWT");
-					}
-				} else {
-					logger.info("Answer contains no new refresh token, keeping old one");
-				}
-
-				BearerAccessToken bearerAccessToken = oidcTokenResponse.getOIDCTokens().getBearerAccessToken();
-
-				HTTPResponse httpResponse = new UserInfoRequest(opMetadataBean.getUserInfoEndpointURI(rpConfig),
-						bearerAccessToken).toHTTPRequest().send();
-
-				UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
-
-				if (!userInfoResponse.indicatesSuccess()) {
-					throw new UserUpdateException("got userinfo error response: "
-							+ userInfoResponse.toErrorResponse().getErrorObject().getDescription());
-				}
-
-				UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo();
-				logger.info("userinfo {}, {}, {}", userInfo.getSubject(), userInfo.getPreferredUsername(),
-						userInfo.getEmailAddress());
-
-				logger.debug("Updating OIDC user {}", user.getSubjectId());
-
-				user = updateUser(user, claims, userInfo, refreshToken, bearerAccessToken, "web-sso", debugLog, null);
-
-			}
-		} catch (IOException | ParseException e) {
-			logger.warn("Exception!", e);
-		}
-
-		return user;
-	}
-
-	@Override
-	public OidcUserEntity updateUser(OidcUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		return updateUser(user, attributeMap, executor, null, null, lastLoginHost);
-	}
-
-	@Override
-	public OidcUserEntity updateUser(OidcUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			ServiceEntity service, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		MDC.put("userId", "" + user.getId());
-		logger.debug("Updating OIDC user {}", user.getEppn());
-
-		boolean changed = false;
-
-		UserUpdateAuditor auditor = new UserUpdateAuditor(auditDao, auditDetailDao, appConfig);
-		auditor.startAuditTrail(executor);
-		auditor.setName(getClass().getName() + "-UserUpdate-Audit");
-		auditor.setDetail("Update OIDC user " + user.getSubjectId());
-
-		changed |= preUpdateUser(user, attributeMap, user.getIssuer().getGenericStore(), executor, service, debugLog);
-
-		// List to store parent services, that are not registered. Need to be registered
-		// later, when attribute map is populated
-		List<ServiceEntity> delayedRegisterList = new ArrayList<ServiceEntity>();
-
-		/**
-		 * put no_assertion_count in generic store if assertion is missing. Else reset
-		 * no assertion count and put last valid assertion date in
-		 */
-		if (attributeMap == null) {
-			if (!user.getGenericStore().containsKey("no_assertion_count")) {
-				user.getGenericStore().put("no_assertion_count", "1");
-			} else {
-				user.getGenericStore().put("no_assertion_count",
-						"" + (Long.parseLong(user.getGenericStore().get("no_assertion_count")) + 1L));
-			}
-
-			logger.info("No attribute for user {}, skipping updateFromAttribute", user.getEppn());
-
-			user.getAttributeStore().clear();
-
-			if (UserStatus.ACTIVE.equals(user.getUserStatus())) {
-				changeUserStatus(user, UserStatus.ON_HOLD, auditor);
-
-				/*
-				 * Also flag all registries for user ON_HOLD
-				 */
-				List<RegistryEntity> registryList = registryDao.findByUserAndStatus(user, RegistryStatus.ACTIVE,
-						RegistryStatus.LOST_ACCESS, RegistryStatus.INVALID);
-				for (RegistryEntity registry : registryList) {
-					changeRegistryStatus(registry, RegistryStatus.ON_HOLD, "user-on-hold", auditor);
-				}
-			}
-		} else {
-			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-			user.getGenericStore().put("no_assertion_count", "0");
-			user.getGenericStore().put("last_valid_assertion", df.format(new Date()));
-
-			changed |= updateUserFromAttribute(user, attributeMap, auditor);
-
-			if (UserStatus.ON_HOLD.equals(user.getUserStatus())) {
-				changeUserStatus(user, UserStatus.ACTIVE, auditor);
-
-				/*
-				 * Also reenable all registries for user to LOST_ACCESS. They are rechecked then
-				 */
-				List<RegistryEntity> registryList = registryDao.findByUserAndStatus(user, RegistryStatus.ON_HOLD);
-				for (RegistryEntity registry : registryList) {
-					changeRegistryStatus(registry, RegistryStatus.LOST_ACCESS, "user-reactivated", auditor);
-
-					/*
-					 * check if parent registry is missing
-					 */
-					if (registry.getService().getParentService() != null) {
-						List<RegistryEntity> parentRegistryList = registryDao.findByServiceAndIdentityAndNotStatus(
-								registry.getService().getParentService(), user.getIdentity(), RegistryStatus.DELETED,
-								RegistryStatus.DEPROVISIONED);
-						if (parentRegistryList.size() == 0) {
-							delayedRegisterList.add(registry.getService().getParentService());
-						}
-					}
-				}
-
-				/*
-				 * fire a user changed event to be sure, when the user is activated
-				 */
-				changed = true;
-			}
-
-			/*
-			 * if service is set, update only attribute sources spcific for this service.
-			 * Else update all (login via web or generic attribute query)
-			 */
-			if (service != null) {
-				service = serviceService.findByIdWithAttrs(service.getId(), ServiceEntity_.attributeSourceService);
-
-				for (AttributeSourceServiceEntity asse : service.getAttributeSourceService()) {
-					changed |= attributeSourceQueryService.updateUserAttributes(user, asse.getAttributeSource(),
-							executor);
-				}
-			} else {
-				List<ASUserAttrEntity> asUserAttrList = asUserAttrDao.findForUser(user);
-				for (ASUserAttrEntity asUserAttr : asUserAttrList) {
-					changed |= attributeSourceQueryService.updateUserAttributes(user, asUserAttr.getAttributeSource(),
-							executor);
-				}
-			}
-
-			Set<GroupEntity> changedGroups = oidcGroupUpdater.updateGroupsForUser(user, attributeMap, auditor);
-
-			if (changedGroups.size() > 0) {
-				changed = true;
-			}
-
-			Map<String, String> attributeStore = user.getAttributeStore();
-			for (Entry<String, List<Object>> entry : attributeMap.entrySet()) {
-				attributeStore.put(entry.getKey(), attrHelper.attributeListToString(entry.getValue()));
-			}
-
-			IncomingAttributeSetEntity incomingAttributeSet = incomingAttributeHandler.createOrUpdateAttributes(user, attributeMap);
-			incomingAttributeHandler.processIncomingAttributeSet(incomingAttributeSet);
-
-			identityUpdater.updateIdentity(user);
-			
-			if (appConfig.getConfigValue("create_missing_eppn_scope") != null) {
-				if (user.getEppn() == null) {
-					String scope = appConfig.getConfigValue("create_missing_eppn_scope");
-					user.setEppn(user.getIdentity().getGeneratedLocalUsername() + "@" + scope);
-					changed = true;
-				}
-			}
-		}
-
-		for (ServiceEntity delayedService : delayedRegisterList) {
-			try {
-				registrator.registerUser(user, delayedService, "user-" + user.getId(), false);
-			} catch (RegisterException e) {
-				logger.warn("Parent registrytion didn't work out like it should", e);
-			}
-		}
-
-		changed |= postUpdateUser(user, attributeMap, user.getIssuer().getGenericStore(), executor, service, debugLog,
-				lastLoginHost);
-
-		user.setLastUpdate(new Date());
-		user.setLastFailedUpdate(null);
-		//user.setExpireWarningSent(null);
-		//user.setExpiredSent(null);
-		user.setScheduledUpdate(getNextScheduledUpdate());
-
-		if (changed) {
-			fireUserChangeEvent(user, auditor.getActualExecutor(), auditor);
-		}
-
-		auditor.setUser(user);
-		auditor.finishAuditTrail();
-		auditor.commitAuditTrail();
-
-		return user;
-	}
-
-	public OidcUserEntity updateUser(OidcUserEntity user, IDTokenClaimsSet claims, UserInfo userInfo,
-			RefreshToken refreshToken, BearerAccessToken bat, String executor, ServiceEntity service,
-			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-
-		Map<String, List<Object>> attributeMap = oidcTokenHelper.convertToAttributeMap(claims, userInfo, refreshToken,
-				bat);
-
-		if (service != null)
-			return updateUser(user, attributeMap, executor, service, debugLog, lastLoginHost);
-		else
-			return updateUser(user, attributeMap, executor, debugLog, lastLoginHost);
-	}
-
-	public OidcUserEntity updateUser(OidcUserEntity user, IDTokenClaimsSet claims, UserInfo userInfo,
-			RefreshToken refreshToken, BearerAccessToken bat, String executor, StringBuffer debugLog,
-			String lastLoginHost) throws UserUpdateException {
-
-		return updateUser(user, claims, userInfo, refreshToken, bat, executor, null, debugLog, lastLoginHost);
-	}
-
-	protected void fireUserChangeEvent(UserEntity user, String executor, Auditor auditor) {
-
-		UserEvent userEvent = new UserEvent(user, auditor.getAudit());
-
-		try {
-			eventSubmitter.submit(userEvent, EventType.USER_UPDATE, executor);
-		} catch (EventSubmitException e) {
-			logger.warn("Could not submit event", e);
-		}
-	}
-
-	public boolean updateUserNew(OidcUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			Auditor auditor, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		boolean changed = false;
-
-		changed |= preUpdateUser(user, attributeMap, user.getIssuer().getGenericStore(), executor, null, debugLog);
-		changed |= updateUserFromAttribute(user, attributeMap, auditor);
-		changed |= postUpdateUser(user, attributeMap, user.getIssuer().getGenericStore(), executor, null, debugLog,
-				lastLoginHost);
-
-		return changed;
-	}
-
-	public boolean updateUserFromAttribute(UserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
-			throws UserUpdateException {
-		return updateUserFromAttribute(user, attributeMap, false, auditor);
-	}
-
-	public boolean updateUserFromAttribute(UserEntity user, Map<String, List<Object>> attributeMap,
-			boolean withoutUidNumber, Auditor auditor) throws UserUpdateException {
-
-		boolean changed = false;
-
-		UserServiceHook completeOverrideHook = null;
-		Set<UserServiceHook> activeHooks = new HashSet<UserServiceHook>();
-
-		for (UserServiceHook hook : hookManager.getUserHooks()) {
-			if (hook.isResponsible(user, attributeMap)) {
-
-				hook.preUpdateUserFromAttribute(user, attributeMap, auditor);
-				activeHooks.add(hook);
-
-				if (hook.isCompleteOverride()) {
-					completeOverrideHook = hook;
-				}
-			}
-		}
-
-		if (completeOverrideHook == null) {
-			IDTokenClaimsSet claims = oidcTokenHelper.claimsFromMap(attributeMap);
-			if (claims == null) {
-				logger.info("No claims set for user {}", user.getId());
-			}
-
-			UserInfo userInfo = oidcTokenHelper.userInfoFromMap(attributeMap);
-			if (userInfo == null) {
-				throw new UserUpdateException("User info is missing in session");
-			}
-
-			changed |= compareAndChangeProperty(user, "email", userInfo.getEmailAddress(), auditor);
-			changed |= compareAndChangeProperty(user, "eppn", userInfo.getStringClaim("eduPersonPrincipalName"),
-					auditor);
-			changed |= compareAndChangeProperty(user, "givenName", userInfo.getGivenName(), auditor);
-			changed |= compareAndChangeProperty(user, "surName", userInfo.getFamilyName(), auditor);
-
-			if ((!withoutUidNumber) && (user.getUidNumber() == null)) {
-				user.setUidNumber(serialDao.nextUidNumber().intValue());
-				logger.info("Setting UID Number {} for user {}", user.getUidNumber(), user.getEppn());
-				auditor.logAction(user.getEppn(), "SET FIELD", "uidNumber", "" + user.getUidNumber(),
-						AuditStatus.SUCCESS);
-				changed = true;
-			}
-		} else {
-			logger.info("Overriding standard User Update Mechanism! Activator: {}",
-					completeOverrideHook.getClass().getName());
-		}
-
-		for (UserServiceHook hook : activeHooks) {
-			hook.postUpdateUserFromAttribute(user, attributeMap, auditor);
-		}
-
-		return changed;
-	}
-
-	private boolean compareAndChangeProperty(UserEntity user, String property, String value, Auditor auditor) {
-		String s = null;
-		String action = null;
-
-		try {
-			Object actualValue = PropertyUtils.getProperty(user, property);
-
-			if (actualValue != null && actualValue.equals(value)) {
-				// Value didn't change, do nothing
-				return false;
-			}
-
-			if (actualValue == null && value == null) {
-				// Value stayed null
-				return false;
-			}
-
-			if (actualValue == null) {
-				s = "null";
-				action = "SET FIELD";
-			} else {
-				s = actualValue.toString();
-				action = "UPDATE FIELD";
-			}
-
-			s = s + " -> " + value;
-			if (s.length() > 1017)
-				s = s.substring(0, 1017) + "...";
-
-			PropertyUtils.setProperty(user, property, value);
-
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.SUCCESS);
-		} catch (IllegalAccessException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		} catch (InvocationTargetException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		} catch (NoSuchMethodException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		}
-
-		return true;
-	}
-
-	protected void changeUserStatus(UserEntity user, UserStatus toStatus, Auditor auditor) {
-		UserStatus fromStatus = user.getUserStatus();
-		user.setUserStatus(toStatus);
-		user.setLastStatusChange(new Date());
-
-		logger.debug("{}: change user status from {} to {}", user.getEppn(), fromStatus, toStatus);
-		auditor.logAction(user.getEppn(), "CHANGE STATUS", fromStatus + " -> " + toStatus,
-				"Change status " + fromStatus + " -> " + toStatus, AuditStatus.SUCCESS);
-	}
-
-	protected void changeRegistryStatus(RegistryEntity registry, RegistryStatus toStatus, String statusMessage,
-			Auditor parentAuditor) {
-		RegistryStatus fromStatus = registry.getRegistryStatus();
-		registry.setRegistryStatus(toStatus);
-		registry.setStatusMessage(statusMessage);
-		registry.setLastStatusChange(new Date());
-
-		logger.debug("{} {} {}: change registry status from {} to {}", new Object[] { registry.getUser().getEppn(),
-				registry.getService().getShortName(), registry.getId(), fromStatus, toStatus });
-		RegistryAuditor registryAuditor = new RegistryAuditor(auditDao, auditDetailDao, appConfig);
-		registryAuditor.setParent(parentAuditor);
-		registryAuditor.startAuditTrail(parentAuditor.getActualExecutor());
-		registryAuditor.setName(getClass().getName() + "-UserUpdate-Registry-Audit");
-		registryAuditor.setDetail("Update registry " + registry.getId() + " for user " + registry.getUser().getEppn());
-		registryAuditor.setRegistry(registry);
-		registryAuditor.logAction(registry.getUser().getEppn(), "CHANGE STATUS", "registry-" + registry.getId(),
-				"Change status " + fromStatus + " -> " + toStatus, AuditStatus.SUCCESS);
-		registryAuditor.finishAuditTrail();
-	}
-
-	private Date getNextScheduledUpdate() {
-		Long futureMillis = 30L * 24L * 60L * 60L * 1000L;
-		if (appConfig.getConfigOptions().containsKey("update_schedule_future")) {
-			futureMillis = Long.decode(appConfig.getConfigValue("update_schedule_future"));
-		}
-		Integer futureMillisRandom = 6 * 60 * 60 * 1000;
-		if (appConfig.getConfigOptions().containsKey("update_schedule_future_random")) {
-			futureMillisRandom = Integer.decode(appConfig.getConfigValue("update_schedule_future_random"));
-		}
-		Random r = new Random();
-		return new Date(System.currentTimeMillis() + futureMillis + r.nextInt(futureMillisRandom));
-	}
-
-	protected void updateFail(OidcUserEntity user) {
-		user.setLastFailedUpdate(new Date());
-		user.setScheduledUpdate(getNextScheduledUpdate());
-	}
-}
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlSpPostServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlSpPostServiceImpl.java
index 422d22b70c0636d7435203f289d389cef7a02365..17d908eeb3b79a53af4f43b5e2e870a73b695a05 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlSpPostServiceImpl.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlSpPostServiceImpl.java
@@ -29,7 +29,7 @@ import edu.kit.scc.webreg.entity.UserLoginInfoEntity;
 import edu.kit.scc.webreg.entity.UserLoginInfoStatus;
 import edu.kit.scc.webreg.entity.UserLoginMethod;
 import edu.kit.scc.webreg.exc.UserUpdateException;
-import edu.kit.scc.webreg.service.impl.UserUpdater;
+import edu.kit.scc.webreg.service.impl.SamlUserUpdater;
 import edu.kit.scc.webreg.service.saml.exc.OidcAuthenticationException;
 import edu.kit.scc.webreg.service.saml.exc.SamlAuthenticationException;
 import edu.kit.scc.webreg.session.SessionManager;
@@ -48,7 +48,7 @@ public class SamlSpPostServiceImpl implements SamlSpPostService {
 	private UserLoginInfoDao userLoginInfoDao;
 
 	@Inject
-	private UserUpdater userUpdater;
+	private SamlUserUpdater userUpdater;
 
 	@Inject
 	private SamlHelper samlHelper;
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/AbstractHomeOrgGroupUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/AbstractHomeOrgGroupUpdater.java
new file mode 100644
index 0000000000000000000000000000000000000000..d25e917604eb8d8d591e7feb374f7eac95fa46f0
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/AbstractHomeOrgGroupUpdater.java
@@ -0,0 +1,19 @@
+package edu.kit.scc.webreg.service.group;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import edu.kit.scc.webreg.audit.Auditor;
+import edu.kit.scc.webreg.entity.GroupEntity;
+import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.exc.UserUpdateException;
+
+public abstract class AbstractHomeOrgGroupUpdater<T extends UserEntity> implements HomeOrgGroupUpdater<T>, Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	public abstract Set<GroupEntity> updateGroupsForUser(T user, Map<String, List<Object>> attributeMap, Auditor auditor)
+			throws UserUpdateException;
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/HomeOrgGroupUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/HomeOrgGroupUpdater.java
index 59c628efa2e7c2eaf7f3607e5df5509c05ee5b9b..c9b1fedfe14675909d41823b58a3ba940c8bf249 100644
--- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/HomeOrgGroupUpdater.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/HomeOrgGroupUpdater.java
@@ -1,385 +1,16 @@
-/*******************************************************************************
- * Copyright (c) 2014 Michael Simon.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the GNU Public License v3.0
- * which accompanies this distribution, and is available at
- * http://www.gnu.org/licenses/gpl.html
- * 
- * Contributors:
- *     Michael Simon - initial
- ******************************************************************************/
 package edu.kit.scc.webreg.service.group;
 
-import java.io.Serializable;
-import java.text.Normalizer;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-
-import org.slf4j.Logger;
-
 import edu.kit.scc.webreg.audit.Auditor;
-import edu.kit.scc.webreg.dao.GroupDao;
-import edu.kit.scc.webreg.dao.HomeOrgGroupDao;
-import edu.kit.scc.webreg.dao.SerialDao;
-import edu.kit.scc.webreg.dao.ServiceGroupFlagDao;
-import edu.kit.scc.webreg.entity.EventType;
 import edu.kit.scc.webreg.entity.GroupEntity;
-import edu.kit.scc.webreg.entity.HomeOrgGroupEntity;
-import edu.kit.scc.webreg.entity.SamlUserEntity;
-import edu.kit.scc.webreg.entity.ServiceBasedGroupEntity;
-import edu.kit.scc.webreg.entity.ServiceGroupFlagEntity;
-import edu.kit.scc.webreg.entity.ServiceGroupStatus;
-import edu.kit.scc.webreg.entity.UserGroupEntity;
-import edu.kit.scc.webreg.entity.audit.AuditStatus;
-import edu.kit.scc.webreg.event.EventSubmitter;
-import edu.kit.scc.webreg.event.MultipleGroupEvent;
-import edu.kit.scc.webreg.event.exc.EventSubmitException;
+import edu.kit.scc.webreg.entity.UserEntity;
 import edu.kit.scc.webreg.exc.UserUpdateException;
-import edu.kit.scc.webreg.hook.GroupServiceHook;
-import edu.kit.scc.webreg.hook.HookManager;
-import edu.kit.scc.webreg.service.identity.HomeIdResolver;
-import edu.kit.scc.webreg.service.impl.AttributeMapHelper;
-
-@ApplicationScoped
-public class HomeOrgGroupUpdater implements Serializable {
-
-	private static final long serialVersionUID = 1L;
-
-	@Inject
-	private Logger logger;
-
-	@Inject
-	private HookManager hookManager;
-	
-	@Inject
-	private HomeOrgGroupDao dao;
-	
-	@Inject
-	private GroupDao groupDao;
-
-	@Inject
-	private ServiceGroupFlagDao groupFlagDao;
-	
-	@Inject
-	private AttributeMapHelper attrHelper;
-
-	@Inject 
-	private SerialDao serialDao;
-
-	@Inject 
-	private EventSubmitter eventSubmitter;
-	
-	@Inject
-	private HomeIdResolver homeIdResolver;
-	
-	public Set<GroupEntity> updateGroupsForUser(SamlUserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
-			throws UserUpdateException {
-
-		HashSet<GroupEntity> changedGroups = new HashSet<GroupEntity>();
-		
-		changedGroups.addAll(updatePrimary(user, attributeMap, auditor));
-		changedGroups.addAll(updateSecondary(user, attributeMap, auditor));
-
-		// Also add parent groups, to reflect changes
-		HashSet<GroupEntity> allChangedGroups = new HashSet<GroupEntity>(changedGroups.size());
-		for (GroupEntity group : changedGroups) {
-			allChangedGroups.add(group);
-			if (group.getParents() != null) {
-				for (GroupEntity parent : group.getParents()) {
-					logger.debug("Adding parent group to changed groups: {}", parent.getName());
-					allChangedGroups.add(parent);
-				}
-			}
-		}
-		
-		for (GroupEntity group : allChangedGroups) {
-			if (group instanceof ServiceBasedGroupEntity) {
-				List<ServiceGroupFlagEntity> groupFlagList = groupFlagDao.findByGroup((ServiceBasedGroupEntity) group);
-				for (ServiceGroupFlagEntity groupFlag : groupFlagList) {
-					groupFlag.setStatus(ServiceGroupStatus.DIRTY);
-					groupFlagDao.persist(groupFlag);
-				}
-			}
-		}
-
-		// do not send group event, if there are not changed groups
-		if (allChangedGroups.size() > 0) {
-			MultipleGroupEvent mge = new MultipleGroupEvent(allChangedGroups);
-			try {
-				eventSubmitter.submit(mge, EventType.GROUP_UPDATE, auditor.getActualExecutor());
-			} catch (EventSubmitException e) {
-				logger.warn("Exeption", e);
-			}
-		}
-		
-		return allChangedGroups;
-	}
-	
-	protected Set<GroupEntity> updatePrimary(SamlUserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
-			throws UserUpdateException {
-		Set<GroupEntity> changedGroups = new HashSet<GroupEntity>();
-
-		GroupServiceHook completeOverrideHook = null;
-		Set<GroupServiceHook> activeHooks = new HashSet<GroupServiceHook>();
-
-		GroupEntity group = null;
-
-		for (GroupServiceHook hook : hookManager.getGroupHooks()) {
-			if (hook.isPrimaryResponsible(user, attributeMap)) {
-				group = hook.preUpdateUserPrimaryGroupFromAttribute(dao, groupDao, group, user, attributeMap, auditor, changedGroups);
-				activeHooks.add(hook);
-				
-				if (hook.isPrimaryCompleteOverride()) {
-					completeOverrideHook = hook;
-				}
-			}
-		}
-		
-		if (completeOverrideHook == null) {
-			
-			String homeId = homeIdResolver.resolveHomeId(user, attributeMap);
-			
-			if (homeId == null) {
-				logger.warn("No Home ID is set for User {}, resetting primary group", user.getEppn());
-			}
-			else {
-				//Filter all non character from homeid
-				homeId = homeId.toLowerCase();
-				homeId = homeId.replaceAll("[^a-z0-9]", "");
-
-				String groupName = homeIdResolver.resolvePrimaryGroup(homeId, user, attributeMap);
-
-				if (groupName == null) {
-					groupName = attrHelper.getSingleStringFirst(attributeMap, "http://bwidm.de/bwidmCC");
-				}
-
-				if (groupName == null) {
-					groupName = homeId;
-				}
-				else {
-					//Filter all non character from groupName
-					groupName = Normalizer.normalize(groupName, Normalizer.Form.NFD);
-					groupName = groupName.toLowerCase();
-					groupName = groupName.replaceAll("[^a-z0-9\\-_]", "");
-				}
-				
-				logger.info("Setting standard HomeID group {} for user {}", homeId, user.getEppn());
-				group = dao.findByNameAndPrefix(groupName, homeId);
-				
-				if (group == null) {
-					HomeOrgGroupEntity homeGroup = dao.createNew();
-					homeGroup.setUsers(new HashSet<UserGroupEntity>());
-					homeGroup.setName(groupName);
-					auditor.logAction(homeGroup.getName(), "SET FIELD", "name", homeGroup.getName(), AuditStatus.SUCCESS);
-					homeGroup.setPrefix(homeId);
-					auditor.logAction(homeGroup.getName(), "SET FIELD", "prefix", homeGroup.getPrefix(), AuditStatus.SUCCESS);
-					homeGroup.setGidNumber(serialDao.next("gid-number-serial").intValue());
-					auditor.logAction(homeGroup.getName(), "SET FIELD", "gidNumber", "" + homeGroup.getGidNumber(), AuditStatus.SUCCESS);
-					homeGroup.setIdp(user.getIdp());
-					auditor.logAction(homeGroup.getName(), "SET FIELD", "idpEntityId", "" + user.getIdp().getEntityId(), AuditStatus.SUCCESS);
-					group = groupDao.persistWithServiceFlags(homeGroup);
-					auditor.logAction(group.getName(), "CREATE GROUP", null, "Group created", AuditStatus.SUCCESS);
-					
-					changedGroups.add(group);
-				}
-			}
-		}
-		else {
-			logger.info("Overriding standard Primary Group Update Mechanism! Activator: {}", completeOverrideHook.getClass().getName());
-		}
-		
-		if (group == null) {
-			logger.warn("No Primary Group for user {}", user.getEppn());
-		}
-
-		for (GroupServiceHook hook : activeHooks) {
-			group = hook.postUpdateUserPrimaryGroupFromAttribute(dao, groupDao, group, user, attributeMap, auditor, changedGroups);
-		}
-		
-		if (user.getPrimaryGroup() != null && (! user.getPrimaryGroup().equals(group))) {
-			if (group == null) {
-				auditor.logAction(user.getEppn(), "UPDATE FIELD", "primaryGroup", 
-						user.getPrimaryGroup().getName() + " (" + user.getPrimaryGroup().getGidNumber() + ") -> " + 
-						"null", AuditStatus.SUCCESS);
-			}
-			else {
-				auditor.logAction(user.getEppn(), "UPDATE FIELD", "primaryGroup", 
-					user.getPrimaryGroup().getName() + " (" + user.getPrimaryGroup().getGidNumber() + ") -> " + 
-					group.getName() + " (" + group.getGidNumber() + ")", AuditStatus.SUCCESS);
-				changedGroups.add(group);
-			}
-		}
-		else if (user.getPrimaryGroup() == null && group != null) {
-			auditor.logAction(user.getEppn(), "UPDATE FIELD", "primaryGroup", 
-					"null -> " + 
-					group.getName() + " (" + group.getGidNumber() + ")", AuditStatus.SUCCESS);
-			changedGroups.add(group);
-		}
-
-		user.setPrimaryGroup(group);
-		
-		return changedGroups;
-	}	
-
-	protected Set<GroupEntity> updateSecondary(SamlUserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
-			throws UserUpdateException {
-		Set<GroupEntity> changedGroups = new HashSet<GroupEntity>();
-
-		GroupServiceHook completeOverrideHook = null;
-		Set<GroupServiceHook> activeHooks = new HashSet<GroupServiceHook>();
-
-		for (GroupServiceHook hook : hookManager.getGroupHooks()) {
-			if (hook.isSecondaryResponsible(user, attributeMap)) {
-				hook.preUpdateUserSecondaryGroupFromAttribute(dao, groupDao, user, attributeMap, auditor, changedGroups);
-				activeHooks.add(hook);
-				
-				if (hook.isSecondaryCompleteOverride()) {
-					completeOverrideHook = hook;
-				}
-			}
-		}
-				
-		if (completeOverrideHook == null) {
-
-			String homeId = homeIdResolver.resolveHomeId(user, attributeMap);
-	
-			List<String> groupList = new ArrayList<String>();
-
-			if (homeId == null) {
-				logger.warn("No Home ID is set for User {}, resetting secondary groups", user.getEppn());
-			}
-			else if (attributeMap.get("http://bwidm.de/bwidmMemberOf") == null) {
-				logger.info("No http://bwidm.de/bwidmMemberOf is set. Resetting secondary groups");
-			}
-			else {
-				List<String> groupsFromAttr = attrHelper.attributeListToStringList(attributeMap, "http://bwidm.de/bwidmMemberOf");
-				
-				//Check if a group name contains a ';', and divide this group
-				for (String group : groupsFromAttr) {
-					if (group.contains(";")) {
-						String[] splitGroups = group.split(";");
-						for (String g : splitGroups) {
-							groupList.add(filterGroup(g));
-						}
-					}
-					else {
-						groupList.add(filterGroup(group));
-					}
-				}
-			}
-			
-			if (user.getGroups() == null)
-				user.setGroups(new HashSet<UserGroupEntity>());
-
-			Set<GroupEntity> groupsFromAssertion = new HashSet<GroupEntity>();
-
-			logger.debug("Looking up groups from database");
-			Map<String, HomeOrgGroupEntity> dbGroupMap = new HashMap<String, HomeOrgGroupEntity>();
-			logger.debug("Indexing groups from database");
-			for (HomeOrgGroupEntity dbGroup : dao.findByNameListAndPrefix(groupList, homeId)) {
-				dbGroupMap.put(dbGroup.getName(), dbGroup);
-			}
-			
-			for (String group : groupList) {
-				if (group != null && (!group.equals(""))) {
-
-					logger.debug("Analyzing group {}", group);
-					HomeOrgGroupEntity groupEntity = dbGroupMap.get(group);
-					
-					try {
-						if (groupEntity == null) {
-							int gidNumber = serialDao.next("gid-number-serial").intValue();
-							logger.info("Creating group {} with gidNumber {}", group, gidNumber);
-							groupEntity = dao.createNew();
-
-							groupEntity.setUsers(new HashSet<UserGroupEntity>());
-							groupEntity.setParents(new HashSet<GroupEntity>());
-							groupEntity.setName(group);
-							auditor.logAction(groupEntity.getName(), "SET FIELD", "name", groupEntity.getName(), AuditStatus.SUCCESS);
-							groupEntity.setPrefix(homeId);
-							auditor.logAction(groupEntity.getName(), "SET FIELD", "prefix", groupEntity.getPrefix(), AuditStatus.SUCCESS);
-							groupEntity.setGidNumber(gidNumber);
-							auditor.logAction(groupEntity.getName(), "SET FIELD", "gidNumber", "" + groupEntity.getGidNumber(), AuditStatus.SUCCESS);
-							groupEntity.setIdp(user.getIdp());
-							auditor.logAction(groupEntity.getName(), "SET FIELD", "idpEntityId", "" + user.getIdp().getEntityId(), AuditStatus.SUCCESS);
-							groupEntity = (HomeOrgGroupEntity) groupDao.persistWithServiceFlags(groupEntity);
-							auditor.logAction(groupEntity.getName(), "CREATE GROUP", null, "Group created", AuditStatus.SUCCESS);
-							
-							changedGroups.add(groupEntity);
-						}
-						
-						if (groupEntity != null) {
-							groupsFromAssertion.add(groupEntity);
-
-							if (! groupDao.isUserInGroup(user, groupEntity)) {
-								logger.debug("Adding user {} to group {}", user.getEppn(), groupEntity.getName());
-								groupDao.addUserToGroup(user, groupEntity);
-								changedGroups.remove(groupEntity);
-								//groupEntity = dao.persist(groupEntity);
-								auditor.logAction(user.getEppn(), "ADD TO GROUP", groupEntity.getName(), null, AuditStatus.SUCCESS);
-
-								changedGroups.add(groupEntity);
-							}
-						}
-						
-					} catch (NumberFormatException e) {
-						logger.warn("GidNumber has a bad number format: {}", e.getMessage());
-					}
-				}
-			}
-			
-			Set<GroupEntity> groupsToRemove = new HashSet<GroupEntity>(groupDao.findByUser(user));
-			groupsToRemove.removeAll(groupsFromAssertion);
 
-			for (GroupEntity removeGroup : groupsToRemove) {
-				if (removeGroup instanceof HomeOrgGroupEntity) {
-					if (! removeGroup.equals(user.getPrimaryGroup())) {
-						logger.debug("Removing user {} from group {}", user.getEppn(), removeGroup.getName());
-						groupDao.removeUserGromGroup(user, removeGroup);
-						
-						auditor.logAction(user.getEppn(), "REMOVE FROM GROUP", removeGroup.getName(), null, AuditStatus.SUCCESS);
-	
-						changedGroups.add(removeGroup);
-					}
-				}
-				else {
-					logger.debug("Group {} of type {}. Doing nothing.", removeGroup.getName(), removeGroup.getClass().getSimpleName());
-				}
-			}
+public interface HomeOrgGroupUpdater<T extends UserEntity> {
 
-			/*
-			 * Add Primary group to secondary as well
-			 */
-			if (user.getPrimaryGroup() != null && (! groupDao.isUserInGroup(user, user.getPrimaryGroup()))) {
-				logger.debug("Adding user {} to his primary group {} as secondary", user.getEppn(), user.getPrimaryGroup().getName());
-				groupDao.addUserToGroup(user, user.getPrimaryGroup());
-				changedGroups.add(user.getPrimaryGroup());
-			}
-		}
-		else {
-			logger.info("Overriding standard Secondary Group Update Mechanism! Activator: {}", completeOverrideHook.getClass().getName());
-		}
-			
-		for (GroupServiceHook hook : activeHooks) {
-			hook.postUpdateUserSecondaryGroupFromAttribute(dao, groupDao, user, attributeMap, auditor, changedGroups);
-		}
-		
-		return changedGroups;
-	}
-	
-	private String filterGroup(String groupName) {
-		//Filter all non character from groupName
-		groupName = Normalizer.normalize(groupName, Normalizer.Form.NFD);
-		groupName = groupName.toLowerCase();
-		groupName = groupName.replaceAll("[^a-z0-9\\-_]", "");
-		
-		return groupName;
-	}	
+	public Set<GroupEntity> updateGroupsForUser(T user, Map<String, List<Object>> attributeMap, Auditor auditor)
+			throws UserUpdateException;
 }
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthGroupUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/OAuthGroupUpdater.java
similarity index 93%
rename from bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthGroupUpdater.java
rename to regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/OAuthGroupUpdater.java
index c4a2c5cda4c7afbc8f90c6915732937f224f2975..1607d2ecd9b649476d18f6ad23a116c489bcd1d0 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oauth/client/OAuthGroupUpdater.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/OAuthGroupUpdater.java
@@ -1,4 +1,4 @@
-package edu.kit.scc.webreg.service.oauth.client;
+package edu.kit.scc.webreg.service.group;
 
 import static edu.kit.scc.webreg.dao.ops.RqlExpressions.and;
 import static edu.kit.scc.webreg.dao.ops.RqlExpressions.equal;
@@ -26,9 +26,6 @@ import edu.kit.scc.webreg.entity.audit.AuditStatus;
 import edu.kit.scc.webreg.entity.oauth.OAuthGroupEntity;
 import edu.kit.scc.webreg.entity.oauth.OAuthGroupEntity_;
 import edu.kit.scc.webreg.entity.oauth.OAuthUserEntity;
-import edu.kit.scc.webreg.entity.oidc.OidcGroupEntity;
-import edu.kit.scc.webreg.entity.oidc.OidcGroupEntity_;
-import edu.kit.scc.webreg.entity.oidc.OidcUserEntity;
 import edu.kit.scc.webreg.event.EventSubmitter;
 import edu.kit.scc.webreg.event.MultipleGroupEvent;
 import edu.kit.scc.webreg.event.exc.EventSubmitException;
@@ -37,7 +34,9 @@ import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 
 @ApplicationScoped
-public class OAuthGroupUpdater {
+public class OAuthGroupUpdater extends AbstractHomeOrgGroupUpdater<OAuthUserEntity> {
+
+	private static final long serialVersionUID = 1L;
 
 	@Inject
 	private Logger logger;
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcGroupUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/OidcGroupUpdater.java
similarity index 94%
rename from bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcGroupUpdater.java
rename to regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/OidcGroupUpdater.java
index 59801039e50e4073d8f437e86bc24f6ad13a7d83..f259fc869e6e546c8b80507b2b8dfead7c34c4c9 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcGroupUpdater.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/OidcGroupUpdater.java
@@ -1,4 +1,4 @@
-package edu.kit.scc.webreg.service.oidc.client;
+package edu.kit.scc.webreg.service.group;
 
 import static edu.kit.scc.webreg.dao.ops.RqlExpressions.and;
 import static edu.kit.scc.webreg.dao.ops.RqlExpressions.equal;
@@ -9,9 +9,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-
 import org.slf4j.Logger;
 
 import edu.kit.scc.webreg.audit.Auditor;
@@ -32,11 +29,14 @@ import edu.kit.scc.webreg.entity.oidc.OidcUserEntity;
 import edu.kit.scc.webreg.event.EventSubmitter;
 import edu.kit.scc.webreg.event.MultipleGroupEvent;
 import edu.kit.scc.webreg.event.exc.EventSubmitException;
-import edu.kit.scc.webreg.service.SerialService;
 import edu.kit.scc.webreg.service.identity.HomeIdResolver;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
 
 @ApplicationScoped
-public class OidcGroupUpdater {
+public class OidcGroupUpdater extends AbstractHomeOrgGroupUpdater<OidcUserEntity> {
+
+	private static final long serialVersionUID = 1L;
 
 	@Inject
 	private Logger logger;
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/SamlGroupUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/SamlGroupUpdater.java
new file mode 100644
index 0000000000000000000000000000000000000000..45aa2b976736e211fc5ef7e6b48848c39bc355de
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/group/SamlGroupUpdater.java
@@ -0,0 +1,383 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Michael Simon.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ * 
+ * Contributors:
+ *     Michael Simon - initial
+ ******************************************************************************/
+package edu.kit.scc.webreg.service.group;
+
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+
+import edu.kit.scc.webreg.audit.Auditor;
+import edu.kit.scc.webreg.dao.GroupDao;
+import edu.kit.scc.webreg.dao.HomeOrgGroupDao;
+import edu.kit.scc.webreg.dao.SerialDao;
+import edu.kit.scc.webreg.dao.ServiceGroupFlagDao;
+import edu.kit.scc.webreg.entity.EventType;
+import edu.kit.scc.webreg.entity.GroupEntity;
+import edu.kit.scc.webreg.entity.HomeOrgGroupEntity;
+import edu.kit.scc.webreg.entity.SamlUserEntity;
+import edu.kit.scc.webreg.entity.ServiceBasedGroupEntity;
+import edu.kit.scc.webreg.entity.ServiceGroupFlagEntity;
+import edu.kit.scc.webreg.entity.ServiceGroupStatus;
+import edu.kit.scc.webreg.entity.UserGroupEntity;
+import edu.kit.scc.webreg.entity.audit.AuditStatus;
+import edu.kit.scc.webreg.event.EventSubmitter;
+import edu.kit.scc.webreg.event.MultipleGroupEvent;
+import edu.kit.scc.webreg.event.exc.EventSubmitException;
+import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.hook.GroupServiceHook;
+import edu.kit.scc.webreg.hook.HookManager;
+import edu.kit.scc.webreg.service.identity.HomeIdResolver;
+import edu.kit.scc.webreg.service.impl.AttributeMapHelper;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class SamlGroupUpdater extends AbstractHomeOrgGroupUpdater<SamlUserEntity> {
+
+	private static final long serialVersionUID = 1L;
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private HookManager hookManager;
+	
+	@Inject
+	private HomeOrgGroupDao dao;
+	
+	@Inject
+	private GroupDao groupDao;
+
+	@Inject
+	private ServiceGroupFlagDao groupFlagDao;
+	
+	@Inject
+	private AttributeMapHelper attrHelper;
+
+	@Inject 
+	private SerialDao serialDao;
+
+	@Inject 
+	private EventSubmitter eventSubmitter;
+	
+	@Inject
+	private HomeIdResolver homeIdResolver;
+	
+	public Set<GroupEntity> updateGroupsForUser(SamlUserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
+			throws UserUpdateException {
+
+		HashSet<GroupEntity> changedGroups = new HashSet<GroupEntity>();
+		
+		changedGroups.addAll(updatePrimary(user, attributeMap, auditor));
+		changedGroups.addAll(updateSecondary(user, attributeMap, auditor));
+
+		// Also add parent groups, to reflect changes
+		HashSet<GroupEntity> allChangedGroups = new HashSet<GroupEntity>(changedGroups.size());
+		for (GroupEntity group : changedGroups) {
+			allChangedGroups.add(group);
+			if (group.getParents() != null) {
+				for (GroupEntity parent : group.getParents()) {
+					logger.debug("Adding parent group to changed groups: {}", parent.getName());
+					allChangedGroups.add(parent);
+				}
+			}
+		}
+		
+		for (GroupEntity group : allChangedGroups) {
+			if (group instanceof ServiceBasedGroupEntity) {
+				List<ServiceGroupFlagEntity> groupFlagList = groupFlagDao.findByGroup((ServiceBasedGroupEntity) group);
+				for (ServiceGroupFlagEntity groupFlag : groupFlagList) {
+					groupFlag.setStatus(ServiceGroupStatus.DIRTY);
+					groupFlagDao.persist(groupFlag);
+				}
+			}
+		}
+
+		// do not send group event, if there are not changed groups
+		if (allChangedGroups.size() > 0) {
+			MultipleGroupEvent mge = new MultipleGroupEvent(allChangedGroups);
+			try {
+				eventSubmitter.submit(mge, EventType.GROUP_UPDATE, auditor.getActualExecutor());
+			} catch (EventSubmitException e) {
+				logger.warn("Exeption", e);
+			}
+		}
+		
+		return allChangedGroups;
+	}
+	
+	protected Set<GroupEntity> updatePrimary(SamlUserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
+			throws UserUpdateException {
+		Set<GroupEntity> changedGroups = new HashSet<GroupEntity>();
+
+		GroupServiceHook completeOverrideHook = null;
+		Set<GroupServiceHook> activeHooks = new HashSet<GroupServiceHook>();
+
+		GroupEntity group = null;
+
+		for (GroupServiceHook hook : hookManager.getGroupHooks()) {
+			if (hook.isPrimaryResponsible(user, attributeMap)) {
+				group = hook.preUpdateUserPrimaryGroupFromAttribute(dao, groupDao, group, user, attributeMap, auditor, changedGroups);
+				activeHooks.add(hook);
+				
+				if (hook.isPrimaryCompleteOverride()) {
+					completeOverrideHook = hook;
+				}
+			}
+		}
+		
+		if (completeOverrideHook == null) {
+			
+			String homeId = homeIdResolver.resolveHomeId(user, attributeMap);
+			
+			if (homeId == null) {
+				logger.warn("No Home ID is set for User {}, resetting primary group", user.getEppn());
+			}
+			else {
+				//Filter all non character from homeid
+				homeId = homeId.toLowerCase();
+				homeId = homeId.replaceAll("[^a-z0-9]", "");
+
+				String groupName = homeIdResolver.resolvePrimaryGroup(homeId, user, attributeMap);
+
+				if (groupName == null) {
+					groupName = attrHelper.getSingleStringFirst(attributeMap, "http://bwidm.de/bwidmCC");
+				}
+
+				if (groupName == null) {
+					groupName = homeId;
+				}
+				else {
+					//Filter all non character from groupName
+					groupName = Normalizer.normalize(groupName, Normalizer.Form.NFD);
+					groupName = groupName.toLowerCase();
+					groupName = groupName.replaceAll("[^a-z0-9\\-_]", "");
+				}
+				
+				logger.info("Setting standard HomeID group {} for user {}", homeId, user.getEppn());
+				group = dao.findByNameAndPrefix(groupName, homeId);
+				
+				if (group == null) {
+					HomeOrgGroupEntity homeGroup = dao.createNew();
+					homeGroup.setUsers(new HashSet<UserGroupEntity>());
+					homeGroup.setName(groupName);
+					auditor.logAction(homeGroup.getName(), "SET FIELD", "name", homeGroup.getName(), AuditStatus.SUCCESS);
+					homeGroup.setPrefix(homeId);
+					auditor.logAction(homeGroup.getName(), "SET FIELD", "prefix", homeGroup.getPrefix(), AuditStatus.SUCCESS);
+					homeGroup.setGidNumber(serialDao.next("gid-number-serial").intValue());
+					auditor.logAction(homeGroup.getName(), "SET FIELD", "gidNumber", "" + homeGroup.getGidNumber(), AuditStatus.SUCCESS);
+					homeGroup.setIdp(user.getIdp());
+					auditor.logAction(homeGroup.getName(), "SET FIELD", "idpEntityId", "" + user.getIdp().getEntityId(), AuditStatus.SUCCESS);
+					group = groupDao.persistWithServiceFlags(homeGroup);
+					auditor.logAction(group.getName(), "CREATE GROUP", null, "Group created", AuditStatus.SUCCESS);
+					
+					changedGroups.add(group);
+				}
+			}
+		}
+		else {
+			logger.info("Overriding standard Primary Group Update Mechanism! Activator: {}", completeOverrideHook.getClass().getName());
+		}
+		
+		if (group == null) {
+			logger.warn("No Primary Group for user {}", user.getEppn());
+		}
+
+		for (GroupServiceHook hook : activeHooks) {
+			group = hook.postUpdateUserPrimaryGroupFromAttribute(dao, groupDao, group, user, attributeMap, auditor, changedGroups);
+		}
+		
+		if (user.getPrimaryGroup() != null && (! user.getPrimaryGroup().equals(group))) {
+			if (group == null) {
+				auditor.logAction(user.getEppn(), "UPDATE FIELD", "primaryGroup", 
+						user.getPrimaryGroup().getName() + " (" + user.getPrimaryGroup().getGidNumber() + ") -> " + 
+						"null", AuditStatus.SUCCESS);
+			}
+			else {
+				auditor.logAction(user.getEppn(), "UPDATE FIELD", "primaryGroup", 
+					user.getPrimaryGroup().getName() + " (" + user.getPrimaryGroup().getGidNumber() + ") -> " + 
+					group.getName() + " (" + group.getGidNumber() + ")", AuditStatus.SUCCESS);
+				changedGroups.add(group);
+			}
+		}
+		else if (user.getPrimaryGroup() == null && group != null) {
+			auditor.logAction(user.getEppn(), "UPDATE FIELD", "primaryGroup", 
+					"null -> " + 
+					group.getName() + " (" + group.getGidNumber() + ")", AuditStatus.SUCCESS);
+			changedGroups.add(group);
+		}
+
+		user.setPrimaryGroup(group);
+		
+		return changedGroups;
+	}	
+
+	protected Set<GroupEntity> updateSecondary(SamlUserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
+			throws UserUpdateException {
+		Set<GroupEntity> changedGroups = new HashSet<GroupEntity>();
+
+		GroupServiceHook completeOverrideHook = null;
+		Set<GroupServiceHook> activeHooks = new HashSet<GroupServiceHook>();
+
+		for (GroupServiceHook hook : hookManager.getGroupHooks()) {
+			if (hook.isSecondaryResponsible(user, attributeMap)) {
+				hook.preUpdateUserSecondaryGroupFromAttribute(dao, groupDao, user, attributeMap, auditor, changedGroups);
+				activeHooks.add(hook);
+				
+				if (hook.isSecondaryCompleteOverride()) {
+					completeOverrideHook = hook;
+				}
+			}
+		}
+				
+		if (completeOverrideHook == null) {
+
+			String homeId = homeIdResolver.resolveHomeId(user, attributeMap);
+	
+			List<String> groupList = new ArrayList<String>();
+
+			if (homeId == null) {
+				logger.warn("No Home ID is set for User {}, resetting secondary groups", user.getEppn());
+			}
+			else if (attributeMap.get("http://bwidm.de/bwidmMemberOf") == null) {
+				logger.info("No http://bwidm.de/bwidmMemberOf is set. Resetting secondary groups");
+			}
+			else {
+				List<String> groupsFromAttr = attrHelper.attributeListToStringList(attributeMap, "http://bwidm.de/bwidmMemberOf");
+				
+				//Check if a group name contains a ';', and divide this group
+				for (String group : groupsFromAttr) {
+					if (group.contains(";")) {
+						String[] splitGroups = group.split(";");
+						for (String g : splitGroups) {
+							groupList.add(filterGroup(g));
+						}
+					}
+					else {
+						groupList.add(filterGroup(group));
+					}
+				}
+			}
+			
+			if (user.getGroups() == null)
+				user.setGroups(new HashSet<UserGroupEntity>());
+
+			Set<GroupEntity> groupsFromAssertion = new HashSet<GroupEntity>();
+
+			logger.debug("Looking up groups from database");
+			Map<String, HomeOrgGroupEntity> dbGroupMap = new HashMap<String, HomeOrgGroupEntity>();
+			logger.debug("Indexing groups from database");
+			for (HomeOrgGroupEntity dbGroup : dao.findByNameListAndPrefix(groupList, homeId)) {
+				dbGroupMap.put(dbGroup.getName(), dbGroup);
+			}
+			
+			for (String group : groupList) {
+				if (group != null && (!group.equals(""))) {
+
+					logger.debug("Analyzing group {}", group);
+					HomeOrgGroupEntity groupEntity = dbGroupMap.get(group);
+					
+					try {
+						if (groupEntity == null) {
+							int gidNumber = serialDao.next("gid-number-serial").intValue();
+							logger.info("Creating group {} with gidNumber {}", group, gidNumber);
+							groupEntity = dao.createNew();
+
+							groupEntity.setUsers(new HashSet<UserGroupEntity>());
+							groupEntity.setParents(new HashSet<GroupEntity>());
+							groupEntity.setName(group);
+							auditor.logAction(groupEntity.getName(), "SET FIELD", "name", groupEntity.getName(), AuditStatus.SUCCESS);
+							groupEntity.setPrefix(homeId);
+							auditor.logAction(groupEntity.getName(), "SET FIELD", "prefix", groupEntity.getPrefix(), AuditStatus.SUCCESS);
+							groupEntity.setGidNumber(gidNumber);
+							auditor.logAction(groupEntity.getName(), "SET FIELD", "gidNumber", "" + groupEntity.getGidNumber(), AuditStatus.SUCCESS);
+							groupEntity.setIdp(user.getIdp());
+							auditor.logAction(groupEntity.getName(), "SET FIELD", "idpEntityId", "" + user.getIdp().getEntityId(), AuditStatus.SUCCESS);
+							groupEntity = (HomeOrgGroupEntity) groupDao.persistWithServiceFlags(groupEntity);
+							auditor.logAction(groupEntity.getName(), "CREATE GROUP", null, "Group created", AuditStatus.SUCCESS);
+							
+							changedGroups.add(groupEntity);
+						}
+						
+						if (groupEntity != null) {
+							groupsFromAssertion.add(groupEntity);
+
+							if (! groupDao.isUserInGroup(user, groupEntity)) {
+								logger.debug("Adding user {} to group {}", user.getEppn(), groupEntity.getName());
+								groupDao.addUserToGroup(user, groupEntity);
+								changedGroups.remove(groupEntity);
+								//groupEntity = dao.persist(groupEntity);
+								auditor.logAction(user.getEppn(), "ADD TO GROUP", groupEntity.getName(), null, AuditStatus.SUCCESS);
+
+								changedGroups.add(groupEntity);
+							}
+						}
+						
+					} catch (NumberFormatException e) {
+						logger.warn("GidNumber has a bad number format: {}", e.getMessage());
+					}
+				}
+			}
+			
+			Set<GroupEntity> groupsToRemove = new HashSet<GroupEntity>(groupDao.findByUser(user));
+			groupsToRemove.removeAll(groupsFromAssertion);
+
+			for (GroupEntity removeGroup : groupsToRemove) {
+				if (removeGroup instanceof HomeOrgGroupEntity) {
+					if (! removeGroup.equals(user.getPrimaryGroup())) {
+						logger.debug("Removing user {} from group {}", user.getEppn(), removeGroup.getName());
+						groupDao.removeUserGromGroup(user, removeGroup);
+						
+						auditor.logAction(user.getEppn(), "REMOVE FROM GROUP", removeGroup.getName(), null, AuditStatus.SUCCESS);
+	
+						changedGroups.add(removeGroup);
+					}
+				}
+				else {
+					logger.debug("Group {} of type {}. Doing nothing.", removeGroup.getName(), removeGroup.getClass().getSimpleName());
+				}
+			}
+
+			/*
+			 * Add Primary group to secondary as well
+			 */
+			if (user.getPrimaryGroup() != null && (! groupDao.isUserInGroup(user, user.getPrimaryGroup()))) {
+				logger.debug("Adding user {} to his primary group {} as secondary", user.getEppn(), user.getPrimaryGroup().getName());
+				groupDao.addUserToGroup(user, user.getPrimaryGroup());
+				changedGroups.add(user.getPrimaryGroup());
+			}
+		}
+		else {
+			logger.info("Overriding standard Secondary Group Update Mechanism! Activator: {}", completeOverrideHook.getClass().getName());
+		}
+			
+		for (GroupServiceHook hook : activeHooks) {
+			hook.postUpdateUserSecondaryGroupFromAttribute(dao, groupDao, user, attributeMap, auditor, changedGroups);
+		}
+		
+		return changedGroups;
+	}
+	
+	private String filterGroup(String groupName) {
+		//Filter all non character from groupName
+		groupName = Normalizer.normalize(groupName, Normalizer.Form.NFD);
+		groupName = groupName.toLowerCase();
+		groupName = groupName.replaceAll("[^a-z0-9\\-_]", "");
+		
+		return groupName;
+	}	
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/AbstractUserUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/AbstractUserUpdater.java
index e7f7cfd5b311a9110f8530e05c2aa1b8d814affd..6745f1f06f8ddacee503b668ba7cb8bf3274084e 100644
--- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/AbstractUserUpdater.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/AbstractUserUpdater.java
@@ -1,72 +1,332 @@
 package edu.kit.scc.webreg.service.impl;
 
+import static edu.kit.scc.webreg.dao.ops.RqlExpressions.equal;
+
 import java.io.Serializable;
 import java.lang.reflect.InvocationTargetException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-
-import jakarta.inject.Inject;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.Set;
 
 import org.slf4j.Logger;
+import org.slf4j.MDC;
 
+import edu.kit.scc.webreg.as.AttributeSourceUpdater;
+import edu.kit.scc.webreg.audit.Auditor;
+import edu.kit.scc.webreg.audit.RegistryAuditor;
+import edu.kit.scc.webreg.audit.UserUpdateAuditor;
+import edu.kit.scc.webreg.bootstrap.ApplicationConfig;
+import edu.kit.scc.webreg.dao.RegistryDao;
+import edu.kit.scc.webreg.dao.ServiceDao;
+import edu.kit.scc.webreg.dao.as.ASUserAttrDao;
+import edu.kit.scc.webreg.dao.as.AttributeSourceDao;
+import edu.kit.scc.webreg.dao.audit.AuditDetailDao;
+import edu.kit.scc.webreg.dao.audit.AuditEntryDao;
+import edu.kit.scc.webreg.entity.EventType;
+import edu.kit.scc.webreg.entity.GroupEntity;
+import edu.kit.scc.webreg.entity.RegistryEntity;
+import edu.kit.scc.webreg.entity.RegistryStatus;
 import edu.kit.scc.webreg.entity.ServiceEntity;
+import edu.kit.scc.webreg.entity.ServiceEntity_;
 import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.entity.UserStatus;
+import edu.kit.scc.webreg.entity.as.ASUserAttrEntity_;
+import edu.kit.scc.webreg.entity.as.AttributeSourceEntity;
+import edu.kit.scc.webreg.entity.as.AttributeSourceEntity_;
+import edu.kit.scc.webreg.entity.as.AttributeSourceServiceEntity;
+import edu.kit.scc.webreg.entity.attribute.IncomingAttributeSetEntity;
+import edu.kit.scc.webreg.entity.audit.AuditDetailEntity;
+import edu.kit.scc.webreg.entity.audit.AuditStatus;
+import edu.kit.scc.webreg.entity.audit.AuditUserUpdateEntity;
+import edu.kit.scc.webreg.event.EventSubmitter;
+import edu.kit.scc.webreg.event.UserEvent;
+import edu.kit.scc.webreg.event.exc.EventSubmitException;
+import edu.kit.scc.webreg.exc.RegisterException;
 import edu.kit.scc.webreg.exc.UserUpdateException;
 import edu.kit.scc.webreg.hook.IdentityScriptingHookWorkflow;
 import edu.kit.scc.webreg.hook.UserUpdateHook;
 import edu.kit.scc.webreg.hook.UserUpdateHookException;
+import edu.kit.scc.webreg.service.attribute.IncomingAttributesHandler;
+import edu.kit.scc.webreg.service.group.HomeOrgGroupUpdater;
 import edu.kit.scc.webreg.service.identity.IdentityScriptingEnv;
+import edu.kit.scc.webreg.service.identity.IdentityUpdater;
+import edu.kit.scc.webreg.service.reg.impl.Registrator;
+import jakarta.inject.Inject;
 
-public abstract class AbstractUserUpdater<T extends UserEntity> implements Serializable {
+public abstract class AbstractUserUpdater<T extends UserEntity> implements UserUpdater<T>, Serializable {
 
 	private static final long serialVersionUID = 1L;
 
 	@Inject
 	private Logger logger;
 
+	@Inject
+	private IdentityUpdater identityUpdater;
+
 	@Inject
 	private IdentityScriptingEnv scriptingEnv;
-	
-	public abstract T updateUser(T user, Map<String, List<Object>> attributeMap, String executor, StringBuffer debugLog, String lastLoginHost)
-			throws UserUpdateException;
 
-	public abstract T updateUser(T user, Map<String, List<Object>> attributeMap, String executor, ServiceEntity service, StringBuffer debugLog, String lastLoginHost)
-			throws UserUpdateException;
+	@Inject
+	private RegistryDao registryDao;
+
+	@Inject
+	private ServiceDao serviceDao;
+
+	@Inject
+	private AttributeSourceDao attributeSourceDao;
+
+	@Inject
+	private ASUserAttrDao asUserAttrDao;
+
+	@Inject
+	private AttributeSourceUpdater attributeSourceUpdater;
+
+	@Inject
+	private AuditEntryDao auditDao;
+
+	@Inject
+	private AuditDetailDao auditDetailDao;
+
+	@Inject
+	private AttributeMapHelper attrHelper;
+
+	@Inject
+	private Registrator registrator;
+
+	@Inject
+	private ApplicationConfig appConfig;
+
+	@Inject
+	private EventSubmitter eventSubmitter;
+
+	public abstract boolean updateUserFromAttribute(T user, Map<String, List<Object>> attributeMap,
+			boolean withoutUidNumber, Auditor auditor) throws UserUpdateException;
+
+	public abstract Map<String, String> resolveHomeOrgGenericStore(T user);
+
+	public abstract IncomingAttributesHandler<?> resolveIncomingAttributeHandler(T user);
+
+	public boolean updateUserFromAttribute(T user, Map<String, List<Object>> attributeMap, Auditor auditor)
+			throws UserUpdateException {
+		return updateUserFromAttribute(user, attributeMap, false, auditor);
+	}
+
+	@Override
+	public T expireUser(T user, String executor) {
+		logger.info("Expiring user {}. Trying one last update", user.getId());
+
+		UserUpdateAuditor auditor = new UserUpdateAuditor(auditDao, auditDetailDao, appConfig);
+		auditor.startAuditTrail(executor);
+		auditor.setName(getClass().getName() + "-UserExpire-Audit");
+		auditor.setDetail("Expire user " + user.getId());
+
+		try {
+			user = updateUserFromHomeOrg(user, null, executor, null);
+
+			// User update from home org did not fail. That means, we don't need to bother
+			// the user to login. Clear the expiry warning, things should be done
+			// automatically
+			user.setExpireWarningSent(null);
+
+			return user;
+
+		} catch (UserUpdateException e) {
+			// The Exception is expected, because the home org will not accept user updates
+			// in the back channel. The user already got an expire warning at this point.
+			user.getAttributeStore().clear();
+
+			// user empty attribute map in order to remove all existing values
+			IncomingAttributeSetEntity incomingAttributeSet = resolveIncomingAttributeHandler(user)
+					.createOrUpdateAttributes(user, new HashMap<>());
+			resolveIncomingAttributeHandler(user).processIncomingAttributeSet(incomingAttributeSet);
+
+			// sets user account on ON_HOLD, if it's in state ACTIVE
+			deactivateUser(user, auditor);
+
+			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+			user.getGenericStore().put("epired_on", df.format(new Date()));
+
+			fireUserChangeEvent(user, auditor.getActualExecutor(), auditor);
+
+			return user;
+		} finally {
+			auditor.setUser(user);
+			auditor.finishAuditTrail();
+			auditor.commitAuditTrail();
+		}
+	}
+
+	public T updateUser(T user, Map<String, List<Object>> attributeMap, String executor, StringBuffer debugLog,
+			String lastLoginHost) throws UserUpdateException {
+		return updateUser(user, attributeMap, executor, null, debugLog, lastLoginHost);
+	}
+
+	public T updateUser(T user, Map<String, List<Object>> attributeMap, String executor, ServiceEntity service,
+			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
+		MDC.put("userId", "" + user.getId());
+		logger.debug("Updating user {} (class: {})", user.getId(), user.getClass().getSimpleName());
+
+		boolean changed = false;
+
+		UserUpdateAuditor auditor = new UserUpdateAuditor(auditDao, auditDetailDao, appConfig);
+		auditor.startAuditTrail(executor);
+		auditor.setName(getClass().getName() + "-UserUpdate-Audit");
+		auditor.setDetail("Update user " + user.getId());
+
+		changed |= preUpdateUser(user, attributeMap, resolveHomeOrgGenericStore(user), executor, service, debugLog);
+
+		// List to store parent services, that are not registered. Need to be registered
+		// later, when attribute map is populated
+		List<ServiceEntity> delayedRegisterList = new ArrayList<ServiceEntity>();
+
+		/**
+		 * put no_assertion_count in generic store if assertion is missing. Else reset
+		 * no assertion count and put last valid assertion date in
+		 */
+		if (attributeMap == null) {
+			if (!user.getGenericStore().containsKey("no_assertion_count")) {
+				user.getGenericStore().put("no_assertion_count", "1");
+			} else {
+				user.getGenericStore().put("no_assertion_count",
+						"" + (Long.parseLong(user.getGenericStore().get("no_assertion_count")) + 1L));
+			}
+
+			logger.info("No attribute for user {}, skipping updateFromAttribute", user.getEppn());
+
+			user.getAttributeStore().clear();
+
+			// user empty attribute map in order to remove all existing values
+			IncomingAttributeSetEntity incomingAttributeSet = resolveIncomingAttributeHandler(user)
+					.createOrUpdateAttributes(user, new HashMap<>());
+			resolveIncomingAttributeHandler(user).processIncomingAttributeSet(incomingAttributeSet);
+
+			// sets user account on ON_HOLD, if it's in state ACTIVE
+			deactivateUser(user, auditor);
+
+		} else {
+			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+			user.getGenericStore().put("no_assertion_count", "0");
+			user.getGenericStore().put("last_valid_assertion", df.format(new Date()));
+
+			changed |= updateUserFromAttribute(user, attributeMap, auditor);
+
+			// if a user is in state ON_HOLD, this reactivates the user to ACTIVE
+			// and sets all registries to LOST_ACCESS in order to be checked again
+			changed |= reactivateUser(user, delayedRegisterList, auditor);
+
+			changed |= updateAttributeSources(user, service, executor, auditor);
+
+			changed |= updateGroups(user, attributeMap, auditor);
+
+			Map<String, String> attributeStore = user.getAttributeStore();
+			attributeStore.clear();
+			for (Entry<String, List<Object>> entry : attributeMap.entrySet()) {
+				attributeStore.put(entry.getKey(), attrHelper.attributeListToString(entry.getValue()));
+			}
+
+			IncomingAttributeSetEntity incomingAttributeSet = resolveIncomingAttributeHandler(user)
+					.createOrUpdateAttributes(user, attributeMap);
+			resolveIncomingAttributeHandler(user).processIncomingAttributeSet(incomingAttributeSet);
+
+			identityUpdater.updateIdentity(user);
 
-	protected boolean preUpdateUser(UserEntity user, Map<String, List<Object>> attributeMap, Map<String,String> homeOrgGenericStore, 
-				String executor, ServiceEntity service, StringBuffer debugLog)
+			if (appConfig.getConfigValue("create_missing_eppn_scope") != null) {
+				if (user.getEppn() == null) {
+					String scope = appConfig.getConfigValue("create_missing_eppn_scope");
+					user.setEppn(user.getIdentity().getGeneratedLocalUsername() + "@" + scope);
+					changed = true;
+				}
+			}
+		}
+
+		for (ServiceEntity delayedService : delayedRegisterList) {
+			try {
+				registrator.registerUser(user, delayedService, "user-" + user.getId(), false);
+			} catch (RegisterException e) {
+				logger.warn("Parent registration didn't work out like it should", e);
+			}
+		}
+
+		changed |= postUpdateUser(user, attributeMap, resolveHomeOrgGenericStore(user), executor, service, debugLog,
+				lastLoginHost);
+
+		user.setLastUpdate(new Date());
+		user.setLastFailedUpdate(null);
+		user.setExpireWarningSent(null);
+		user.setExpiredSent(null);
+		user.setScheduledUpdate(getNextScheduledUpdate());
+
+		if (changed) {
+			fireUserChangeEvent(user, auditor.getActualExecutor(), auditor);
+		}
+
+		auditor.setUser(user);
+		auditor.finishAuditTrail();
+		auditor.commitAuditTrail();
+
+		if (debugLog != null) {
+			AuditUserUpdateEntity audit = auditor.getAudit();
+			debugLog.append("\n\nPrinting audit from user update process:\n\nName: ").append(audit.getName())
+					.append("\nDetail: ").append(audit.getDetail()).append("\n");
+			for (AuditDetailEntity detail : audit.getAuditDetails()) {
+				debugLog.append(detail.getEndTime()).append(" | ").append(detail.getSubject()).append(" | ")
+						.append(detail.getObject()).append(" | ").append(detail.getAction()).append(" | ")
+						.append(detail.getLog()).append(" | ").append(detail.getAuditStatus()).append("\n");
+			}
+
+			if (audit.getAuditDetails().size() == 0) {
+				debugLog.append("Nothing seems to have changed.\n");
+			}
+		}
+
+		return user;
+	}
+
+	public abstract HomeOrgGroupUpdater<T> getGroupUpdater();
+
+	protected boolean preUpdateUser(T user, Map<String, List<Object>> attributeMap,
+			Map<String, String> homeOrgGenericStore, String executor, ServiceEntity service, StringBuffer debugLog)
 			throws UserUpdateException {
 
 		boolean returnValue = false;
-		
+
 		UserUpdateHook updateHook = resolveUpdateHook(homeOrgGenericStore);
-		
+
 		if (updateHook != null) {
 			try {
-				returnValue |= updateHook.preUpdateUser(user, homeOrgGenericStore, attributeMap, executor, service, null);
+				returnValue |= updateHook.preUpdateUser(user, homeOrgGenericStore, attributeMap, executor, service,
+						null);
 			} catch (UserUpdateHookException e) {
 				logger.warn("An exception happened while calling UserUpdateHook!", e);
 			}
 		}
-				
+
 		return returnValue;
 	}
 
-	protected boolean postUpdateUser(UserEntity user, Map<String, List<Object>> attributeMap, Map<String,String> homeOrgGenericStore, 
-				String executor, ServiceEntity service, StringBuffer debugLog, String lastLoginHost)
-			throws UserUpdateException {
+	protected boolean postUpdateUser(T user, Map<String, List<Object>> attributeMap,
+			Map<String, String> homeOrgGenericStore, String executor, ServiceEntity service, StringBuffer debugLog,
+			String lastLoginHost) throws UserUpdateException {
 
 		boolean returnValue = false;
 
 		if (lastLoginHost != null) {
 			user.setLastLoginHost(lastLoginHost);
 		}
-		
+
 		UserUpdateHook updateHook = resolveUpdateHook(homeOrgGenericStore);
 
 		if (updateHook != null) {
 			try {
-				returnValue |= updateHook.postUpdateUser(user, homeOrgGenericStore, attributeMap, executor, service, null);
+				returnValue |= updateHook.postUpdateUser(user, homeOrgGenericStore, attributeMap, executor, service,
+						null);
 			} catch (UserUpdateHookException e) {
 				logger.warn("An exception happened while calling UserUpdateHook!", e);
 			}
@@ -74,7 +334,7 @@ public abstract class AbstractUserUpdater<T extends UserEntity> implements Seria
 		return returnValue;
 	}
 
-	private UserUpdateHook resolveUpdateHook(Map<String,String> homeOrgGenericStore) {
+	private UserUpdateHook resolveUpdateHook(Map<String, String> homeOrgGenericStore) {
 		UserUpdateHook updateHook = null;
 		if (homeOrgGenericStore.containsKey("user_update_hook")) {
 			String hookClass = homeOrgGenericStore.get("user_update_hook");
@@ -89,7 +349,151 @@ public abstract class AbstractUserUpdater<T extends UserEntity> implements Seria
 				logger.warn("Cannot instantiate updateHook class. This is probably a misconfiguration.");
 			}
 		}
-		
+
 		return updateHook;
 	}
+
+	protected void deactivateUser(T user, Auditor auditor) {
+		if (UserStatus.ACTIVE.equals(user.getUserStatus())) {
+			changeUserStatus(user, UserStatus.ON_HOLD, auditor);
+
+			identityUpdater.updateIdentity(user);
+
+			/*
+			 * Also flag all registries for user ON_HOLD
+			 */
+			List<RegistryEntity> registryList = registryDao.findByUserAndStatus(user, RegistryStatus.ACTIVE,
+					RegistryStatus.LOST_ACCESS, RegistryStatus.INVALID);
+			for (RegistryEntity registry : registryList) {
+				changeRegistryStatus(registry, RegistryStatus.ON_HOLD, "user-on-hold", auditor);
+			}
+		}
+	}
+
+	protected boolean reactivateUser(T user, List<ServiceEntity> delayedRegisterList, Auditor auditor) {
+		Boolean changed = false;
+		if (UserStatus.ON_HOLD.equals(user.getUserStatus())) {
+			changeUserStatus(user, UserStatus.ACTIVE, auditor);
+
+			/*
+			 * Also reenable all registries for user to LOST_ACCESS. They are rechecked then
+			 */
+			List<RegistryEntity> registryList = registryDao.findByUserAndStatus(user, RegistryStatus.ON_HOLD);
+			for (RegistryEntity registry : registryList) {
+				changeRegistryStatus(registry, RegistryStatus.LOST_ACCESS, "user-reactivated", auditor);
+
+				/*
+				 * check if parent registry is missing
+				 */
+				if (registry.getService().getParentService() != null) {
+					List<RegistryEntity> parentRegistryList = registryDao.findByServiceAndIdentityAndNotStatus(
+							registry.getService().getParentService(), user.getIdentity(), RegistryStatus.DELETED,
+							RegistryStatus.DEPROVISIONED);
+					if (parentRegistryList.size() == 0) {
+						delayedRegisterList.add(registry.getService().getParentService());
+					}
+				}
+			}
+
+			/*
+			 * fire a user changed event to be sure, when the user is activated
+			 */
+			changed = true;
+		}
+
+		return changed;
+	}
+
+	protected boolean updateGroups(T user, Map<String, List<Object>> attributeMap, Auditor auditor)
+			throws UserUpdateException {
+		Set<GroupEntity> changedGroups = getGroupUpdater().updateGroupsForUser(user, attributeMap, auditor);
+
+		if (changedGroups.size() > 0) {
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+	protected boolean updateAttributeSources(T user, ServiceEntity service, String executor, Auditor auditor)
+			throws UserUpdateException {
+		Boolean changed = false;
+
+		/*
+		 * if service is set, update only attribute sources spcific for this service.
+		 * Else update all (login via web or generic attribute query)
+		 */
+		if (service != null) {
+			service = serviceDao.find(equal(ServiceEntity_.id, service.getId()), ServiceEntity_.attributeSourceService);
+
+			for (AttributeSourceServiceEntity asse : service.getAttributeSourceService()) {
+				changed |= attributeSourceUpdater.updateUserAttributes(user, asse.getAttributeSource(), executor);
+			}
+		} else {
+			// find all user sources to update
+			Set<AttributeSourceEntity> asList = new HashSet<>(
+					attributeSourceDao.findAll(equal(AttributeSourceEntity_.userSource, true)));
+			// and add all sources which are already connected to the user
+			asList.addAll(asUserAttrDao.findAll(equal(ASUserAttrEntity_.user, user)).stream()
+					.map(a -> a.getAttributeSource()).toList());
+			for (AttributeSourceEntity as : asList) {
+				changed |= attributeSourceUpdater.updateUserAttributes(user, as, executor);
+			}
+		}
+		return changed;
+	}
+
+	protected void changeUserStatus(T user, UserStatus toStatus, Auditor auditor) {
+		UserStatus fromStatus = user.getUserStatus();
+		user.setUserStatus(toStatus);
+		user.setLastStatusChange(new Date());
+
+		logger.debug("{}: change user status from {} to {}", user.getEppn(), fromStatus, toStatus);
+		auditor.logAction(user.getEppn(), "CHANGE STATUS", fromStatus + " -> " + toStatus,
+				"Change status " + fromStatus + " -> " + toStatus, AuditStatus.SUCCESS);
+	}
+
+	protected void changeRegistryStatus(RegistryEntity registry, RegistryStatus toStatus, String statusMessage,
+			Auditor parentAuditor) {
+		RegistryStatus fromStatus = registry.getRegistryStatus();
+		registry.setRegistryStatus(toStatus);
+		registry.setStatusMessage(statusMessage);
+		registry.setLastStatusChange(new Date());
+
+		logger.debug("{} {} {}: change registry status from {} to {}", new Object[] { registry.getUser().getEppn(),
+				registry.getService().getShortName(), registry.getId(), fromStatus, toStatus });
+		RegistryAuditor registryAuditor = new RegistryAuditor(auditDao, auditDetailDao, appConfig);
+		registryAuditor.setParent(parentAuditor);
+		registryAuditor.startAuditTrail(parentAuditor.getActualExecutor());
+		registryAuditor.setName(getClass().getName() + "-UserUpdate-Registry-Audit");
+		registryAuditor.setDetail("Update registry " + registry.getId() + " for user " + registry.getUser().getEppn());
+		registryAuditor.setRegistry(registry);
+		registryAuditor.logAction(registry.getUser().getEppn(), "CHANGE STATUS", "registry-" + registry.getId(),
+				"Change status " + fromStatus + " -> " + toStatus, AuditStatus.SUCCESS);
+		registryAuditor.finishAuditTrail();
+	}
+
+	protected Date getNextScheduledUpdate() {
+		Long futureMillis = 30L * 24L * 60L * 60L * 1000L;
+		if (appConfig.getConfigOptions().containsKey("update_schedule_future")) {
+			futureMillis = Long.decode(appConfig.getConfigValue("update_schedule_future"));
+		}
+		Integer futureMillisRandom = 6 * 60 * 60 * 1000;
+		if (appConfig.getConfigOptions().containsKey("update_schedule_future_random")) {
+			futureMillisRandom = Integer.decode(appConfig.getConfigValue("update_schedule_future_random"));
+		}
+		Random r = new Random();
+		return new Date(System.currentTimeMillis() + futureMillis + r.nextInt(futureMillisRandom));
+	}
+
+	protected void fireUserChangeEvent(T user, String executor, Auditor auditor) {
+
+		UserEvent userEvent = new UserEvent(user, auditor.getAudit());
+
+		try {
+			eventSubmitter.submit(userEvent, EventType.USER_UPDATE, executor);
+		} catch (EventSubmitException e) {
+			logger.warn("Could not submit event", e);
+		}
+	}
 }
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/OAuthUserUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/OAuthUserUpdater.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c4b7dcbd020afc5a52d7f209f722896288025db
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/OAuthUserUpdater.java
@@ -0,0 +1,192 @@
+package edu.kit.scc.webreg.service.impl;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.slf4j.Logger;
+
+import edu.kit.scc.webreg.audit.Auditor;
+import edu.kit.scc.webreg.dao.SerialDao;
+import edu.kit.scc.webreg.entity.ServiceEntity;
+import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.entity.attribute.IncomingOAuthAttributeEntity;
+import edu.kit.scc.webreg.entity.audit.AuditStatus;
+import edu.kit.scc.webreg.entity.oauth.OAuthUserEntity;
+import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.hook.HookManager;
+import edu.kit.scc.webreg.hook.UserServiceHook;
+import edu.kit.scc.webreg.service.attribute.IncomingAttributesHandler;
+import edu.kit.scc.webreg.service.attribute.IncomingOAuthAttributesHandler;
+import edu.kit.scc.webreg.service.group.HomeOrgGroupUpdater;
+import edu.kit.scc.webreg.service.group.OAuthGroupUpdater;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class OAuthUserUpdater extends AbstractUserUpdater<OAuthUserEntity> {
+
+	private static final long serialVersionUID = 1L;
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private SerialDao serialDao;
+
+	@Inject
+	private HookManager hookManager;
+
+	@Inject
+	private OAuthGroupUpdater oauthGroupUpdater;
+
+	@Inject
+	private IncomingOAuthAttributesHandler incomingAttributeHandler;
+	
+	public OAuthUserEntity updateUserFromOP(OAuthUserEntity user, String executor, StringBuffer debugLog)
+			throws UserUpdateException {
+		return updateUserFromHomeOrg(user, null, executor, debugLog);
+	}
+
+	public OAuthUserEntity updateUserFromHomeOrg(OAuthUserEntity user, ServiceEntity service, String executor,
+			StringBuffer debugLog) throws UserUpdateException {
+		updateFail(user);
+		throw new UserUpdateException("Not implemented");
+	}
+	
+	public boolean updateUserNew(OAuthUserEntity user, Map<String, List<Object>> attributeMap, String executor,
+			Auditor auditor, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
+		boolean changed = false;
+
+		changed |= preUpdateUser(user, attributeMap, user.getOauthIssuer().getGenericStore(), executor, null, debugLog);
+		changed |= updateUserFromAttribute(user, attributeMap, auditor);
+		changed |= postUpdateUser(user, attributeMap, user.getOauthIssuer().getGenericStore(), executor, null, debugLog,
+				lastLoginHost);
+
+		return changed;
+	}
+
+	public boolean updateUserFromAttribute(OAuthUserEntity user, Map<String, List<Object>> attributeMap,
+			boolean withoutUidNumber, Auditor auditor) throws UserUpdateException {
+
+		boolean changed = false;
+
+		UserServiceHook completeOverrideHook = null;
+		Set<UserServiceHook> activeHooks = new HashSet<UserServiceHook>();
+
+		for (UserServiceHook hook : hookManager.getUserHooks()) {
+			if (hook.isResponsible(user, attributeMap)) {
+
+				hook.preUpdateUserFromAttribute(user, attributeMap, auditor);
+				activeHooks.add(hook);
+
+				if (hook.isCompleteOverride()) {
+					completeOverrideHook = hook;
+				}
+			}
+		}
+
+		if (completeOverrideHook == null) {
+			@SuppressWarnings("unchecked")
+			HashMap<String, Object> userMap = (HashMap<String, Object>) attributeMap.get("user").get(0);
+
+			if (userMap.get("email") != null && (userMap.get("email") instanceof String))
+				changed |= compareAndChangeProperty(user, "email", (String) userMap.get("email"), auditor);
+			else
+				changed |= compareAndChangeProperty(user, "email", null, auditor);
+
+			if (userMap.get("name") != null && (userMap.get("name") instanceof String))
+				changed |= compareAndChangeProperty(user, "name", (String) userMap.get("name"), auditor);
+			else
+				changed |= compareAndChangeProperty(user, "name", null, auditor);
+
+			if ((!withoutUidNumber) && (user.getUidNumber() == null)) {
+				user.setUidNumber(serialDao.nextUidNumber().intValue());
+				logger.info("Setting UID Number {} for user {}", user.getUidNumber(), user.getEppn());
+				auditor.logAction(user.getEppn(), "SET FIELD", "uidNumber", "" + user.getUidNumber(),
+						AuditStatus.SUCCESS);
+				changed = true;
+			}
+		} else {
+			logger.info("Overriding standard User Update Mechanism! Activator: {}",
+					completeOverrideHook.getClass().getName());
+		}
+
+		for (UserServiceHook hook : activeHooks) {
+			hook.postUpdateUserFromAttribute(user, attributeMap, auditor);
+		}
+
+		return changed;
+	}
+
+	private boolean compareAndChangeProperty(UserEntity user, String property, String value, Auditor auditor) {
+		String s = null;
+		String action = null;
+
+		try {
+			Object actualValue = PropertyUtils.getProperty(user, property);
+
+			if (actualValue != null && actualValue.equals(value)) {
+				// Value didn't change, do nothing
+				return false;
+			}
+
+			if (actualValue == null && value == null) {
+				// Value stayed null
+				return false;
+			}
+
+			if (actualValue == null) {
+				s = "null";
+				action = "SET FIELD";
+			} else {
+				s = actualValue.toString();
+				action = "UPDATE FIELD";
+			}
+
+			s = s + " -> " + value;
+			if (s.length() > 1017)
+				s = s.substring(0, 1017) + "...";
+
+			PropertyUtils.setProperty(user, property, value);
+
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.SUCCESS);
+		} catch (IllegalAccessException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		} catch (InvocationTargetException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		} catch (NoSuchMethodException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		}
+
+		return true;
+	}
+
+	protected void updateFail(OAuthUserEntity user) {
+		user.setLastFailedUpdate(new Date());
+		user.setScheduledUpdate(getNextScheduledUpdate());
+	}
+
+	@Override
+	public HomeOrgGroupUpdater<OAuthUserEntity> getGroupUpdater() {
+		return oauthGroupUpdater;
+	}
+
+	@Override
+	public Map<String, String> resolveHomeOrgGenericStore(OAuthUserEntity user) {
+		return user.getOauthIssuer().getGenericStore();
+	}
+
+	@Override
+	public IncomingAttributesHandler<IncomingOAuthAttributeEntity> resolveIncomingAttributeHandler(OAuthUserEntity user) {
+		return incomingAttributeHandler;
+	}
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/OidcUserUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/OidcUserUpdater.java
new file mode 100644
index 0000000000000000000000000000000000000000..07bf647ca523c2ebfbfaf032a25d4bf6243e79df
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/OidcUserUpdater.java
@@ -0,0 +1,344 @@
+package edu.kit.scc.webreg.service.impl;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTParser;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
+import com.nimbusds.oauth2.sdk.TokenErrorResponse;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
+import com.nimbusds.openid.connect.sdk.UserInfoRequest;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
+
+import edu.kit.scc.regapp.oidc.tools.OidcOpMetadataSingletonBean;
+import edu.kit.scc.regapp.oidc.tools.OidcTokenHelper;
+import edu.kit.scc.webreg.audit.Auditor;
+import edu.kit.scc.webreg.dao.SerialDao;
+import edu.kit.scc.webreg.entity.ServiceEntity;
+import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.entity.attribute.IncomingOidcAttributeEntity;
+import edu.kit.scc.webreg.entity.audit.AuditStatus;
+import edu.kit.scc.webreg.entity.oidc.OidcRpConfigurationEntity;
+import edu.kit.scc.webreg.entity.oidc.OidcUserEntity;
+import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.hook.HookManager;
+import edu.kit.scc.webreg.hook.UserServiceHook;
+import edu.kit.scc.webreg.service.attribute.IncomingAttributesHandler;
+import edu.kit.scc.webreg.service.attribute.IncomingOidcAttributesHandler;
+import edu.kit.scc.webreg.service.group.HomeOrgGroupUpdater;
+import edu.kit.scc.webreg.service.group.OidcGroupUpdater;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class OidcUserUpdater extends AbstractUserUpdater<OidcUserEntity> {
+
+	private static final long serialVersionUID = 1L;
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private SerialDao serialDao;
+
+	@Inject
+	private HookManager hookManager;
+
+	@Inject
+	private OidcGroupUpdater oidcGroupUpdater;
+
+	@Inject
+	private OidcTokenHelper oidcTokenHelper;
+
+	@Inject
+	private OidcOpMetadataSingletonBean opMetadataBean;
+	
+	@Inject
+	private IncomingOidcAttributesHandler incomingAttributeHandler;
+
+	public OidcUserEntity updateUserFromOP(OidcUserEntity user, String executor, StringBuffer debugLog)
+			throws UserUpdateException {
+		return updateUserFromHomeOrg(user, null, executor, debugLog);
+	}
+	
+	public OidcUserEntity updateUserFromHomeOrg(OidcUserEntity user, ServiceEntity service, String executor, StringBuffer debugLog)
+			throws UserUpdateException {
+
+		try {
+			/**
+			 * TODO Implement refresh here
+			 */
+			OidcRpConfigurationEntity rpConfig = user.getIssuer();
+
+			if (user.getAttributeStore().get("refreshToken") == null) {
+				updateFail(user);
+				throw new UserUpdateException("refresh token is null");
+			}
+			
+			RefreshToken token = new RefreshToken(user.getAttributeStore().get("refreshToken"));
+			AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(token);
+
+			ClientID clientID = new ClientID(user.getIssuer().getClientId());
+			Secret clientSecret = new Secret(user.getIssuer().getSecret());
+			ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret);
+
+			TokenRequest tokenRequest = new TokenRequest(opMetadataBean.getTokenEndpointURI(user.getIssuer()),
+					clientAuth, refreshTokenGrant);
+			TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send());
+
+			if (!tokenResponse.indicatesSuccess()) {
+				TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
+				ErrorObject error = errorResponse.getErrorObject();
+				logger.info("Got error: code {}, desc {}, http-status {}, uri {}", error.getCode(),
+						error.getDescription());
+				updateFail(user);
+				throw new UserUpdateException();
+			} else {
+				OIDCTokenResponse oidcTokenResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();
+				logger.debug("response: {}", oidcTokenResponse.toJSONObject());
+
+				JWT idToken = oidcTokenResponse.getOIDCTokens().getIDToken();
+				IDTokenClaimsSet claims = null;
+
+				if (idToken != null) {
+					IDTokenValidator validator = new IDTokenValidator(new Issuer(rpConfig.getServiceUrl()),
+							new ClientID(rpConfig.getClientId()), JWSAlgorithm.RS256,
+							opMetadataBean.getJWKSetURI(rpConfig).toURL());
+
+					try {
+						claims = validator.validate(idToken, null);
+						logger.debug("Got signed claims verified from {}: {}", claims.getIssuer(), claims.getSubject());
+					} catch (BadJOSEException | JOSEException e) {
+						throw new UserUpdateException("signature failed: " + e.getMessage());
+					}
+				}
+
+				RefreshToken refreshToken = null;
+
+				if (oidcTokenResponse.getOIDCTokens().getRefreshToken() != null) {
+
+					refreshToken = oidcTokenResponse.getOIDCTokens().getRefreshToken();
+					try {
+						JWT refreshJwt = JWTParser.parse(refreshToken.getValue());
+						// Well, what to do with this info? Check if refresh token is short or long
+						// lived? <1 day?
+						logger.info("refresh will expire at: {}", refreshJwt.getJWTClaimsSet().getExpirationTime());
+					} catch (java.text.ParseException e) {
+						logger.debug("Refresh token is no JWT");
+					}
+				} else {
+					logger.info("Answer contains no new refresh token, keeping old one");
+				}
+
+				BearerAccessToken bearerAccessToken = oidcTokenResponse.getOIDCTokens().getBearerAccessToken();
+
+				HTTPResponse httpResponse = new UserInfoRequest(opMetadataBean.getUserInfoEndpointURI(rpConfig),
+						bearerAccessToken).toHTTPRequest().send();
+
+				UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
+
+				if (!userInfoResponse.indicatesSuccess()) {
+					updateFail(user);
+					throw new UserUpdateException("got userinfo error response: "
+							+ userInfoResponse.toErrorResponse().getErrorObject().getDescription());
+				}
+
+				UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo();
+				logger.info("userinfo {}, {}, {}", userInfo.getSubject(), userInfo.getPreferredUsername(),
+						userInfo.getEmailAddress());
+
+				logger.debug("Updating OIDC user {}", user.getSubjectId());
+
+				user = updateUser(user, claims, userInfo, refreshToken, bearerAccessToken, "web-sso", debugLog, null);
+
+			}
+		} catch (IOException | ParseException e) {
+			logger.warn("Exception!", e);
+		}
+
+		return user;
+	}
+
+	public OidcUserEntity updateUser(OidcUserEntity user, IDTokenClaimsSet claims, UserInfo userInfo,
+			RefreshToken refreshToken, BearerAccessToken bat, String executor, ServiceEntity service,
+			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
+
+		Map<String, List<Object>> attributeMap = oidcTokenHelper.convertToAttributeMap(claims, userInfo, refreshToken,
+				bat);
+
+		if (service != null)
+			return updateUser(user, attributeMap, executor, service, debugLog, lastLoginHost);
+		else
+			return updateUser(user, attributeMap, executor, debugLog, lastLoginHost);
+	}
+
+	public OidcUserEntity updateUser(OidcUserEntity user, IDTokenClaimsSet claims, UserInfo userInfo,
+			RefreshToken refreshToken, BearerAccessToken bat, String executor, StringBuffer debugLog,
+			String lastLoginHost) throws UserUpdateException {
+
+		return updateUser(user, claims, userInfo, refreshToken, bat, executor, null, debugLog, lastLoginHost);
+	}
+
+	public boolean updateUserNew(OidcUserEntity user, Map<String, List<Object>> attributeMap, String executor,
+			Auditor auditor, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
+		boolean changed = false;
+
+		changed |= preUpdateUser(user, attributeMap, user.getIssuer().getGenericStore(), executor, null, debugLog);
+		changed |= updateUserFromAttribute(user, attributeMap, auditor);
+		changed |= postUpdateUser(user, attributeMap, user.getIssuer().getGenericStore(), executor, null, debugLog,
+				lastLoginHost);
+
+		return changed;
+	}
+
+	public boolean updateUserFromAttribute(OidcUserEntity user, Map<String, List<Object>> attributeMap,
+			boolean withoutUidNumber, Auditor auditor) throws UserUpdateException {
+
+		boolean changed = false;
+
+		UserServiceHook completeOverrideHook = null;
+		Set<UserServiceHook> activeHooks = new HashSet<UserServiceHook>();
+
+		for (UserServiceHook hook : hookManager.getUserHooks()) {
+			if (hook.isResponsible(user, attributeMap)) {
+
+				hook.preUpdateUserFromAttribute(user, attributeMap, auditor);
+				activeHooks.add(hook);
+
+				if (hook.isCompleteOverride()) {
+					completeOverrideHook = hook;
+				}
+			}
+		}
+
+		if (completeOverrideHook == null) {
+			IDTokenClaimsSet claims = oidcTokenHelper.claimsFromMap(attributeMap);
+			if (claims == null) {
+				logger.info("No claims set for user {}", user.getId());
+			}
+
+			UserInfo userInfo = oidcTokenHelper.userInfoFromMap(attributeMap);
+			if (userInfo == null) {
+				throw new UserUpdateException("User info is missing in session");
+			}
+
+			changed |= compareAndChangeProperty(user, "email", userInfo.getEmailAddress(), auditor);
+			changed |= compareAndChangeProperty(user, "eppn", userInfo.getStringClaim("eduPersonPrincipalName"),
+					auditor);
+			changed |= compareAndChangeProperty(user, "givenName", userInfo.getGivenName(), auditor);
+			changed |= compareAndChangeProperty(user, "surName", userInfo.getFamilyName(), auditor);
+
+			if ((!withoutUidNumber) && (user.getUidNumber() == null)) {
+				user.setUidNumber(serialDao.nextUidNumber().intValue());
+				logger.info("Setting UID Number {} for user {}", user.getUidNumber(), user.getEppn());
+				auditor.logAction(user.getEppn(), "SET FIELD", "uidNumber", "" + user.getUidNumber(),
+						AuditStatus.SUCCESS);
+				changed = true;
+			}
+		} else {
+			logger.info("Overriding standard User Update Mechanism! Activator: {}",
+					completeOverrideHook.getClass().getName());
+		}
+
+		for (UserServiceHook hook : activeHooks) {
+			hook.postUpdateUserFromAttribute(user, attributeMap, auditor);
+		}
+
+		return changed;
+	}
+
+	private boolean compareAndChangeProperty(UserEntity user, String property, String value, Auditor auditor) {
+		String s = null;
+		String action = null;
+
+		try {
+			Object actualValue = PropertyUtils.getProperty(user, property);
+
+			if (actualValue != null && actualValue.equals(value)) {
+				// Value didn't change, do nothing
+				return false;
+			}
+
+			if (actualValue == null && value == null) {
+				// Value stayed null
+				return false;
+			}
+
+			if (actualValue == null) {
+				s = "null";
+				action = "SET FIELD";
+			} else {
+				s = actualValue.toString();
+				action = "UPDATE FIELD";
+			}
+
+			s = s + " -> " + value;
+			if (s.length() > 1017)
+				s = s.substring(0, 1017) + "...";
+
+			PropertyUtils.setProperty(user, property, value);
+
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.SUCCESS);
+		} catch (IllegalAccessException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		} catch (InvocationTargetException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		} catch (NoSuchMethodException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		}
+
+		return true;
+	}
+
+	protected void updateFail(OidcUserEntity user) {
+		user.setLastFailedUpdate(new Date());
+		user.setScheduledUpdate(getNextScheduledUpdate());
+	}
+
+	@Override
+	public HomeOrgGroupUpdater<OidcUserEntity> getGroupUpdater() {
+		return oidcGroupUpdater;
+	}
+
+	@Override
+	public Map<String, String> resolveHomeOrgGenericStore(OidcUserEntity user) {
+		return user.getIssuer().getGenericStore();
+	}
+
+	@Override
+	public IncomingAttributesHandler<IncomingOidcAttributeEntity> resolveIncomingAttributeHandler(OidcUserEntity user) {
+		return incomingAttributeHandler;
+	}
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/SamlUserUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/SamlUserUpdater.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec61616e8efd606681b090e6e3458c261e0aa1ba
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/SamlUserUpdater.java
@@ -0,0 +1,425 @@
+package edu.kit.scc.webreg.service.impl;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.soap.common.SOAPException;
+import org.opensaml.xmlsec.encryption.support.DecryptionException;
+import org.slf4j.Logger;
+
+import edu.kit.scc.webreg.audit.Auditor;
+import edu.kit.scc.webreg.audit.IdpCommunicationAuditor;
+import edu.kit.scc.webreg.bootstrap.ApplicationConfig;
+import edu.kit.scc.webreg.dao.SamlAssertionDao;
+import edu.kit.scc.webreg.dao.SamlIdpMetadataDao;
+import edu.kit.scc.webreg.dao.SamlSpConfigurationDao;
+import edu.kit.scc.webreg.dao.SamlUserDao;
+import edu.kit.scc.webreg.dao.SerialDao;
+import edu.kit.scc.webreg.dao.audit.AuditDetailDao;
+import edu.kit.scc.webreg.dao.audit.AuditEntryDao;
+import edu.kit.scc.webreg.entity.SamlAssertionEntity;
+import edu.kit.scc.webreg.entity.SamlIdpMetadataEntity;
+import edu.kit.scc.webreg.entity.SamlIdpMetadataEntityStatus;
+import edu.kit.scc.webreg.entity.SamlSpConfigurationEntity;
+import edu.kit.scc.webreg.entity.SamlUserEntity;
+import edu.kit.scc.webreg.entity.ServiceEntity;
+import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.entity.attribute.IncomingSamlAttributeEntity;
+import edu.kit.scc.webreg.entity.audit.AuditStatus;
+import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.hook.HookManager;
+import edu.kit.scc.webreg.hook.UserServiceHook;
+import edu.kit.scc.webreg.logging.LogHelper;
+import edu.kit.scc.webreg.service.attribute.IncomingAttributesHandler;
+import edu.kit.scc.webreg.service.attribute.IncomingSamlAttributesHandler;
+import edu.kit.scc.webreg.service.group.HomeOrgGroupUpdater;
+import edu.kit.scc.webreg.service.group.SamlGroupUpdater;
+import edu.kit.scc.webreg.service.saml.AttributeQueryHelper;
+import edu.kit.scc.webreg.service.saml.Saml2AssertionService;
+import edu.kit.scc.webreg.service.saml.SamlHelper;
+import edu.kit.scc.webreg.service.saml.exc.MetadataException;
+import edu.kit.scc.webreg.service.saml.exc.NoAssertionException;
+import edu.kit.scc.webreg.service.saml.exc.SamlAuthenticationException;
+import edu.kit.scc.webreg.service.saml.exc.SamlUnknownPrincipalException;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class SamlUserUpdater extends AbstractUserUpdater<SamlUserEntity> {
+
+	private static final long serialVersionUID = 1L;
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private AuditEntryDao auditDao;
+
+	@Inject
+	private AuditDetailDao auditDetailDao;
+
+	@Inject
+	private Saml2AssertionService saml2AssertionService;
+
+	@Inject
+	private AttributeQueryHelper attrQueryHelper;
+
+	@Inject
+	private SamlUserDao userDao;
+
+	@Inject
+	private SamlGroupUpdater homeOrgGroupUpdater;
+
+	@Inject
+	private SamlHelper samlHelper;
+
+	@Inject
+	private SamlIdpMetadataDao idpDao;
+
+	@Inject
+	private SamlSpConfigurationDao spDao;
+
+	@Inject
+	private SerialDao serialDao;
+
+	@Inject
+	private HookManager hookManager;
+
+	@Inject
+	private IncomingSamlAttributesHandler incomingAttributeHandler;
+
+	@Inject
+	private SamlAssertionDao samlAsserionDao;
+
+	@Inject
+	private AttributeMapHelper attrHelper;
+
+	@Inject ApplicationConfig appConfig;
+
+	@Inject
+	private LogHelper logHelper;
+
+	public SamlUserEntity updateUser(SamlUserEntity user, Assertion assertion, String executor, ServiceEntity service,
+			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
+
+		if (assertion != null) {
+			samlAsserionDao.deleteAssertionForUser(user);
+
+			SamlAssertionEntity samlAssertionEntity = samlAsserionDao.createNew();
+			samlAssertionEntity.setUser(user);
+			samlAssertionEntity.setAssertionData(samlHelper.prettyPrint(assertion));
+			samlAssertionEntity.setValidUntil(new Date(System.currentTimeMillis() + (4L * 60L * 60L * 1000L)));
+			samlAssertionEntity = samlAsserionDao.persist(samlAssertionEntity);
+		}
+
+		Map<String, List<Object>> attributeMap = saml2AssertionService.extractAttributes(assertion);
+
+		if (debugLog != null) {
+			debugLog.append("Extracted attributes from Assertion:\n");
+			for (Entry<String, List<Object>> entry : attributeMap.entrySet()) {
+				debugLog.append(entry.getKey()).append(":\t").append(entry.getValue()).append("\n");
+			}
+		}
+
+		if (service != null)
+			return updateUser(user, attributeMap, executor, service, debugLog, lastLoginHost);
+		else
+			return updateUser(user, attributeMap, executor, debugLog, lastLoginHost);
+	}
+
+	public SamlUserEntity updateUser(SamlUserEntity user, Assertion assertion, String executor, String lastLoginHost)
+			throws UserUpdateException {
+
+		return updateUser(user, assertion, executor, null, null, lastLoginHost);
+	}
+
+	public SamlUserEntity updateUserFromIdp(SamlUserEntity user, String executor) throws UserUpdateException {
+		return updateUserFromHomeOrg(user, null, executor, null);
+	}
+
+	public SamlUserEntity updateUserFromHomeOrg(SamlUserEntity user, ServiceEntity service, String executor,
+			StringBuffer debugLog) throws UserUpdateException {
+
+		SamlSpConfigurationEntity spEntity = spDao.findByEntityId(user.getPersistentSpId());
+		SamlIdpMetadataEntity idpEntity = idpDao.findByEntityId(user.getIdp().getEntityId());
+
+		IdpCommunicationAuditor auditor = new IdpCommunicationAuditor(auditDao, auditDetailDao, appConfig);
+		auditor.setName("UpdateUserFromIdp");
+		auditor.setDetail("Call IDP " + idpEntity.getEntityId() + " from SP " + spEntity.getEntityId() + " for User "
+				+ user.getEppn());
+		auditor.setIdp(idpEntity);
+		auditor.setSpConfig(spEntity);
+		auditor.startAuditTrail(executor);
+
+		EntityDescriptor idpEntityDescriptor = samlHelper.unmarshal(idpEntity.getEntityDescriptor(),
+				EntityDescriptor.class, auditor);
+
+		Response samlResponse;
+		try {
+			/*
+			 * If something goes wrong here, communication with the idp probably failed
+			 */
+
+			samlResponse = attrQueryHelper.query(user, idpEntity, idpEntityDescriptor, spEntity, debugLog);
+
+			if (logger.isTraceEnabled())
+				logger.trace("{}", samlHelper.prettyPrint(samlResponse));
+
+			if (debugLog != null) {
+				debugLog.append("\nIncoming SAML Response:\n\n").append(samlHelper.prettyPrint(samlResponse))
+						.append("\n");
+			}
+
+		} catch (SOAPException e) {
+			/*
+			 * This exception is thrown if the certificate chain is incomplete e.g.
+			 */
+			handleException(user, e, idpEntity, auditor, debugLog);
+			throw new UserUpdateException(e);
+		} catch (MetadataException e) {
+			/*
+			 * is thrown if AttributeQuery location is missing in metadata, or something is
+			 * wrong with the sp certificate
+			 */
+			handleException(user, e, idpEntity, auditor, debugLog);
+			throw new UserUpdateException(e);
+		} catch (SecurityException e) {
+			handleException(user, e, idpEntity, auditor, debugLog);
+			throw new UserUpdateException(e);
+		} catch (Exception e) {
+			handleException(user, e, idpEntity, auditor, debugLog);
+			throw new UserUpdateException(e);
+		}
+
+		try {
+			/*
+			 * Don't check Assertion Signature, because we are contacting the IDP directly
+			 */
+			Assertion assertion;
+			try {
+				if (debugLog != null) {
+					debugLog.append("\nExtracting Assertion from SAML Response without signature check...\n");
+				}
+
+				assertion = saml2AssertionService.processSamlResponse(samlResponse, idpEntity, idpEntityDescriptor,
+						spEntity, false);
+
+				if (logger.isTraceEnabled())
+					logger.trace("{}", samlHelper.prettyPrint(assertion));
+
+			} catch (NoAssertionException e) {
+				if (user.getIdp() != null)
+					logger.warn("No assertion delivered for user {} from idp {}", user.getEppn(),
+							user.getIdp().getEntityId());
+				else
+					logger.warn("No assertion delivered for user {} from idp {}", user.getEppn());
+				assertion = null;
+			} catch (SamlUnknownPrincipalException e) {
+				if (user.getIdp() != null)
+					logger.warn("Unknown principal status for user {} from idp {}", user.getEppn(),
+							user.getIdp().getEntityId());
+				else
+					logger.warn("Unknown principal status  for user {}", user.getEppn());
+				assertion = null;
+			}
+
+			updateIdpStatus(SamlIdpMetadataEntityStatus.GOOD, idpEntity);
+
+			return updateUser(user, assertion, "attribute-query", service, debugLog, null);
+		} catch (DecryptionException e) {
+			handleException(user, e, idpEntity, auditor, debugLog);
+			throw new UserUpdateException(e);
+		} catch (IOException e) {
+			handleException(user, e, idpEntity, auditor, debugLog);
+			throw new UserUpdateException(e);
+		} catch (SamlAuthenticationException e) {
+			/*
+			 * Thrown if i.e. the AttributeQuery profile is not configured correctly
+			 */
+			handleException(user, e, idpEntity, auditor, debugLog);
+			throw new UserUpdateException(e);
+		}
+	}
+
+	protected void handleException(SamlUserEntity user, Exception e, SamlIdpMetadataEntity idpEntity, Auditor auditor,
+			StringBuffer debugLog) {
+		updateFail(user);
+		String message = e.getMessage();
+		if (e.getCause() != null)
+			message += " InnerCause: " + e.getCause().getMessage();
+		auditor.logAction(idpEntity.getEntityId(), "SAML ATTRIBUTE QUERY", user.getEppn(), message, AuditStatus.FAIL);
+		auditor.finishAuditTrail();
+		auditor.commitAuditTrail();
+
+		if (debugLog != null) {
+			debugLog.append("Attribute Query failed: ").append(e.getMessage());
+			if (e.getCause() != null)
+				debugLog.append("Cause: ").append(e.getCause().getMessage());
+			debugLog.append(logHelper.convertStacktrace(e));
+		}
+
+		updateIdpStatus(SamlIdpMetadataEntityStatus.FAULTY, idpEntity);
+	}
+
+	protected void updateIdpStatus(SamlIdpMetadataEntityStatus status, SamlIdpMetadataEntity idpEntity) {
+		if (!status.equals(idpEntity.getAqIdpStatus())) {
+			idpEntity.setAqIdpStatus(status);
+			idpEntity.setLastAqStatusChange(new Date());
+		}
+	}
+
+	protected void updateFail(SamlUserEntity user) {
+		user.setLastFailedUpdate(new Date());
+		user.setScheduledUpdate(getNextScheduledUpdate());
+		user = userDao.persist(user);
+	}
+
+
+
+	public boolean updateUserNew(SamlUserEntity user, Map<String, List<Object>> attributeMap, String executor,
+			Auditor auditor, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
+		boolean changed = false;
+
+		changed |= preUpdateUser(user, attributeMap, user.getIdp().getGenericStore(), executor, null, debugLog);
+		changed |= updateUserFromAttribute(user, attributeMap, auditor);
+		changed |= postUpdateUser(user, attributeMap, user.getIdp().getGenericStore(), executor, null, debugLog,
+				lastLoginHost);
+
+		return changed;
+	}
+
+	public boolean updateUserFromAttribute(SamlUserEntity user, Map<String, List<Object>> attributeMap,
+			boolean withoutUidNumber, Auditor auditor) throws UserUpdateException {
+
+		boolean changed = false;
+
+		UserServiceHook completeOverrideHook = null;
+		Set<UserServiceHook> activeHooks = new HashSet<UserServiceHook>();
+
+		for (UserServiceHook hook : hookManager.getUserHooks()) {
+			if (hook.isResponsible(user, attributeMap)) {
+
+				hook.preUpdateUserFromAttribute(user, attributeMap, auditor);
+				activeHooks.add(hook);
+
+				if (hook.isCompleteOverride()) {
+					completeOverrideHook = hook;
+				}
+			}
+		}
+
+		if (completeOverrideHook == null) {
+			changed |= compareAndChangeProperty(user, "email", attributeMap.get("urn:oid:0.9.2342.19200300.100.1.3"),
+					auditor);
+			changed |= compareAndChangeProperty(user, "eppn", attributeMap.get("urn:oid:1.3.6.1.4.1.5923.1.1.1.6"),
+					auditor);
+			changed |= compareAndChangeProperty(user, "givenName", attributeMap.get("urn:oid:2.5.4.42"), auditor);
+			changed |= compareAndChangeProperty(user, "surName", attributeMap.get("urn:oid:2.5.4.4"), auditor);
+
+			List<String> emailList = attrHelper.attributeListToStringList(attributeMap,
+					"urn:oid:0.9.2342.19200300.100.1.3");
+			if (emailList != null && emailList.size() > 1) {
+
+				if (user.getEmailAddresses() == null) {
+					user.setEmailAddresses(new HashSet<String>());
+				}
+
+				for (int i = 1; i < emailList.size(); i++) {
+					user.getEmailAddresses().add(emailList.get(i));
+				}
+			}
+
+			if ((!withoutUidNumber) && (user.getUidNumber() == null)) {
+				user.setUidNumber(serialDao.nextUidNumber().intValue());
+				logger.info("Setting UID Number {} for user {}", user.getUidNumber(), user.getEppn());
+				auditor.logAction(user.getEppn(), "SET FIELD", "uidNumber", "" + user.getUidNumber(),
+						AuditStatus.SUCCESS);
+				changed = true;
+			}
+		} else {
+			logger.info("Overriding standard User Update Mechanism! Activator: {}",
+					completeOverrideHook.getClass().getName());
+		}
+
+		for (UserServiceHook hook : activeHooks) {
+			hook.postUpdateUserFromAttribute(user, attributeMap, auditor);
+		}
+
+		return changed;
+	}
+
+	private boolean compareAndChangeProperty(UserEntity user, String property, List<Object> objectValue,
+			Auditor auditor) {
+		String s = null;
+		String action = null;
+
+		// In case of a List (multiple SAML Values), take the first value
+		String value = attrHelper.getSingleStringFirst(objectValue);
+
+		try {
+			Object actualValue = PropertyUtils.getProperty(user, property);
+
+			if (actualValue != null && actualValue.equals(value)) {
+				// Value didn't change, do nothing
+				return false;
+			}
+
+			if (actualValue == null && value == null) {
+				// Value stayed null
+				return false;
+			}
+
+			if (actualValue == null) {
+				s = "null";
+				action = "SET FIELD";
+			} else {
+				s = actualValue.toString();
+				action = "UPDATE FIELD";
+			}
+
+			s = s + " -> " + value;
+			if (s.length() > 1017)
+				s = s.substring(0, 1017) + "...";
+
+			PropertyUtils.setProperty(user, property, value);
+
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.SUCCESS);
+		} catch (IllegalAccessException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		} catch (InvocationTargetException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		} catch (NoSuchMethodException e) {
+			logger.warn("This probably shouldn't happen: ", e);
+			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
+		}
+
+		return true;
+	}
+
+	@Override
+	public HomeOrgGroupUpdater<SamlUserEntity> getGroupUpdater() {
+		return homeOrgGroupUpdater;
+	}
+
+	@Override
+	public Map<String, String> resolveHomeOrgGenericStore(SamlUserEntity user) {
+		return user.getIdp().getGenericStore();
+	}
+
+	@Override
+	public IncomingAttributesHandler<IncomingSamlAttributeEntity> resolveIncomingAttributeHandler(SamlUserEntity user) {
+		return incomingAttributeHandler;
+	}
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdater.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdater.java
index 8119a7d9e1e2e28f6c802edaf02f47bdea658d05..b4f6b4e1bc9f84bf9aacbcf09ea0e69ccb81be2f 100644
--- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdater.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/impl/UserUpdater.java
@@ -1,708 +1,22 @@
-package edu.kit.scc.webreg.service.impl;
-
-import static edu.kit.scc.webreg.dao.ops.RqlExpressions.equal;
-
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Random;
-import java.util.Set;
-
-import org.apache.commons.beanutils.PropertyUtils;
-import org.opensaml.saml.saml2.core.Assertion;
-import org.opensaml.saml.saml2.core.Response;
-import org.opensaml.saml.saml2.metadata.EntityDescriptor;
-import org.opensaml.soap.common.SOAPException;
-import org.opensaml.xmlsec.encryption.support.DecryptionException;
-import org.slf4j.Logger;
-import org.slf4j.MDC;
-
-import edu.kit.scc.webreg.as.AttributeSourceUpdater;
-import edu.kit.scc.webreg.audit.Auditor;
-import edu.kit.scc.webreg.audit.IdpCommunicationAuditor;
-import edu.kit.scc.webreg.audit.RegistryAuditor;
-import edu.kit.scc.webreg.audit.UserUpdateAuditor;
-import edu.kit.scc.webreg.bootstrap.ApplicationConfig;
-import edu.kit.scc.webreg.dao.RegistryDao;
-import edu.kit.scc.webreg.dao.SamlAssertionDao;
-import edu.kit.scc.webreg.dao.SamlIdpMetadataDao;
-import edu.kit.scc.webreg.dao.SamlSpConfigurationDao;
-import edu.kit.scc.webreg.dao.SamlUserDao;
-import edu.kit.scc.webreg.dao.SerialDao;
-import edu.kit.scc.webreg.dao.ServiceDao;
-import edu.kit.scc.webreg.dao.as.ASUserAttrDao;
-import edu.kit.scc.webreg.dao.as.AttributeSourceDao;
-import edu.kit.scc.webreg.dao.audit.AuditDetailDao;
-import edu.kit.scc.webreg.dao.audit.AuditEntryDao;
-import edu.kit.scc.webreg.entity.EventType;
-import edu.kit.scc.webreg.entity.GroupEntity;
-import edu.kit.scc.webreg.entity.RegistryEntity;
-import edu.kit.scc.webreg.entity.RegistryStatus;
-import edu.kit.scc.webreg.entity.SamlAssertionEntity;
-import edu.kit.scc.webreg.entity.SamlIdpMetadataEntity;
-import edu.kit.scc.webreg.entity.SamlIdpMetadataEntityStatus;
-import edu.kit.scc.webreg.entity.SamlSpConfigurationEntity;
-import edu.kit.scc.webreg.entity.SamlUserEntity;
-import edu.kit.scc.webreg.entity.ServiceEntity;
-import edu.kit.scc.webreg.entity.ServiceEntity_;
-import edu.kit.scc.webreg.entity.UserEntity;
-import edu.kit.scc.webreg.entity.UserStatus;
-import edu.kit.scc.webreg.entity.as.ASUserAttrEntity_;
-import edu.kit.scc.webreg.entity.as.AttributeSourceEntity;
-import edu.kit.scc.webreg.entity.as.AttributeSourceEntity_;
-import edu.kit.scc.webreg.entity.as.AttributeSourceServiceEntity;
-import edu.kit.scc.webreg.entity.attribute.IncomingAttributeSetEntity;
-import edu.kit.scc.webreg.entity.audit.AuditDetailEntity;
-import edu.kit.scc.webreg.entity.audit.AuditStatus;
-import edu.kit.scc.webreg.entity.audit.AuditUserUpdateEntity;
-import edu.kit.scc.webreg.event.EventSubmitter;
-import edu.kit.scc.webreg.event.UserEvent;
-import edu.kit.scc.webreg.event.exc.EventSubmitException;
-import edu.kit.scc.webreg.exc.RegisterException;
-import edu.kit.scc.webreg.exc.UserUpdateException;
-import edu.kit.scc.webreg.hook.HookManager;
-import edu.kit.scc.webreg.hook.UserServiceHook;
-import edu.kit.scc.webreg.logging.LogHelper;
-import edu.kit.scc.webreg.service.attribute.IncomingSamlAttributesHandler;
-import edu.kit.scc.webreg.service.group.HomeOrgGroupUpdater;
-import edu.kit.scc.webreg.service.identity.IdentityUpdater;
-import edu.kit.scc.webreg.service.impl.AttributeMapHelper;
-import edu.kit.scc.webreg.service.reg.impl.Registrator;
-import edu.kit.scc.webreg.service.saml.AttributeQueryHelper;
-import edu.kit.scc.webreg.service.saml.Saml2AssertionService;
-import edu.kit.scc.webreg.service.saml.SamlHelper;
-import edu.kit.scc.webreg.service.saml.exc.MetadataException;
-import edu.kit.scc.webreg.service.saml.exc.NoAssertionException;
-import edu.kit.scc.webreg.service.saml.exc.SamlAuthenticationException;
-import edu.kit.scc.webreg.service.saml.exc.SamlUnknownPrincipalException;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-
-@ApplicationScoped
-public class UserUpdater extends AbstractUserUpdater<SamlUserEntity> {
-
-	private static final long serialVersionUID = 1L;
-
-	@Inject
-	private Logger logger;
-
-	@Inject
-	private AuditEntryDao auditDao;
-
-	@Inject
-	private AuditDetailDao auditDetailDao;
-
-	@Inject
-	private Saml2AssertionService saml2AssertionService;
-
-	@Inject
-	private AttributeQueryHelper attrQueryHelper;
-
-	@Inject
-	private SamlUserDao userDao;
-
-	@Inject
-	private ServiceDao serviceDao;
-
-	@Inject
-	private RegistryDao registryDao;
-
-	@Inject
-	private HomeOrgGroupUpdater homeOrgGroupUpdater;
-
-	@Inject
-	private SamlHelper samlHelper;
-
-	@Inject
-	private SamlIdpMetadataDao idpDao;
-
-	@Inject
-	private SamlSpConfigurationDao spDao;
-
-	@Inject
-	private SerialDao serialDao;
-
-	@Inject
-	private HookManager hookManager;
-
-	@Inject
-	private AttributeSourceDao attributeSourceDao;
-
-	@Inject
-	private ASUserAttrDao asUserAttrDao;
-
-	@Inject
-	private SamlAssertionDao samlAsserionDao;
-
-	@Inject
-	private AttributeSourceUpdater attributeSourceUpdater;
-
-	@Inject
-	private AttributeMapHelper attrHelper;
-
-	@Inject
-	private EventSubmitter eventSubmitter;
-
-	@Inject
-	private ApplicationConfig appConfig;
-
-	@Inject
-	private Registrator registrator;
-
-	@Inject
-	private IdentityUpdater identityUpdater;
-
-	@Inject
-	private IncomingSamlAttributesHandler incomingAttributeHandler;
-	
-	@Inject
-	private LogHelper logHelper;
-
-	@Override
-	public SamlUserEntity updateUser(SamlUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		return updateUser(user, attributeMap, executor, null, debugLog, lastLoginHost);
-	}
-
-	@Override
-	public SamlUserEntity updateUser(SamlUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			ServiceEntity service, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		MDC.put("userId", "" + user.getId());
-		logger.debug("Updating SAML user {}", user.getEppn());
-
-		boolean changed = false;
-
-		UserUpdateAuditor auditor = new UserUpdateAuditor(auditDao, auditDetailDao, appConfig);
-		auditor.startAuditTrail(executor);
-		auditor.setName(getClass().getName() + "-UserUpdate-Audit");
-		auditor.setDetail("Update user " + user.getEppn());
-
-		changed |= preUpdateUser(user, attributeMap, user.getIdp().getGenericStore(), executor, service, debugLog);
-
-		// List to store parent services, that are not registered. Need to be registered
-		// later, when attribute map is populated
-		List<ServiceEntity> delayedRegisterList = new ArrayList<ServiceEntity>();
-
-		/**
-		 * put no_assertion_count in generic store if assertion is missing. Else reset
-		 * no assertion count and put last valid assertion date in
-		 */
-		if (attributeMap == null) {
-			if (!user.getGenericStore().containsKey("no_assertion_count")) {
-				user.getGenericStore().put("no_assertion_count", "1");
-			} else {
-				user.getGenericStore().put("no_assertion_count",
-						"" + (Long.parseLong(user.getGenericStore().get("no_assertion_count")) + 1L));
-			}
-
-			logger.info("No attribute for user {}, skipping updateFromAttribute", user.getEppn());
-
-			user.getAttributeStore().clear();
-
-			// user empty attribute map in order to remove all existing values
-			IncomingAttributeSetEntity incomingAttributeSet = incomingAttributeHandler.createOrUpdateAttributes(user, new HashMap<>());
-			incomingAttributeHandler.processIncomingAttributeSet(incomingAttributeSet);
-
-			if (UserStatus.ACTIVE.equals(user.getUserStatus())) {
-				changeUserStatus(user, UserStatus.ON_HOLD, auditor);
-
-				identityUpdater.updateIdentity(user);
-
-				/*
-				 * Also flag all registries for user ON_HOLD
-				 */
-				List<RegistryEntity> registryList = registryDao.findByUserAndStatus(user, RegistryStatus.ACTIVE,
-						RegistryStatus.LOST_ACCESS, RegistryStatus.INVALID);
-				for (RegistryEntity registry : registryList) {
-					changeRegistryStatus(registry, RegistryStatus.ON_HOLD, "user-on-hold", auditor);
-				}
-			}
-		} else {
-			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-			user.getGenericStore().put("no_assertion_count", "0");
-			user.getGenericStore().put("last_valid_assertion", df.format(new Date()));
-
-			changed |= updateUserFromAttribute(user, attributeMap, auditor);
-
-			if (UserStatus.ON_HOLD.equals(user.getUserStatus())) {
-				changeUserStatus(user, UserStatus.ACTIVE, auditor);
-
-				/*
-				 * Also reenable all registries for user to LOST_ACCESS. They are rechecked then
-				 */
-				List<RegistryEntity> registryList = registryDao.findByUserAndStatus(user, RegistryStatus.ON_HOLD);
-				for (RegistryEntity registry : registryList) {
-					changeRegistryStatus(registry, RegistryStatus.LOST_ACCESS, "user-reactivated", auditor);
-
-					/*
-					 * check if parent registry is missing
-					 */
-					if (registry.getService().getParentService() != null) {
-						List<RegistryEntity> parentRegistryList = registryDao.findByServiceAndIdentityAndNotStatus(
-								registry.getService().getParentService(), user.getIdentity(), RegistryStatus.DELETED,
-								RegistryStatus.DEPROVISIONED);
-						if (parentRegistryList.size() == 0) {
-							delayedRegisterList.add(registry.getService().getParentService());
-						}
-					}
-				}
-
-				/*
-				 * fire a user changed event to be sure, when the user is activated
-				 */
-				changed = true;
-			}
-
-			/*
-			 * if service is set, update only attribute sources spcific for this service.
-			 * Else update all (login via web or generic attribute query)
-			 */
-			if (service != null) {
-				service = serviceDao.find(equal(ServiceEntity_.id, service.getId()),
-						ServiceEntity_.attributeSourceService);
-
-				for (AttributeSourceServiceEntity asse : service.getAttributeSourceService()) {
-					changed |= attributeSourceUpdater.updateUserAttributes(user, asse.getAttributeSource(), executor);
-				}
-			} else {
-				// find all user sources to update
-				Set<AttributeSourceEntity> asList = new HashSet<>(attributeSourceDao
-						.findAll(equal(AttributeSourceEntity_.userSource, true)));
-				// and add all sources which are already connected to the user
-				asList.addAll(asUserAttrDao.findAll(equal(ASUserAttrEntity_.user, user)).stream()
-						.map(a -> a.getAttributeSource()).toList());
-				for (AttributeSourceEntity as : asList) {
-					changed |= attributeSourceUpdater.updateUserAttributes(user, as, executor);
-				}
-			}
-
-			Set<GroupEntity> changedGroups = homeOrgGroupUpdater.updateGroupsForUser(user, attributeMap, auditor);
-
-			if (changedGroups.size() > 0) {
-				changed = true;
-			}
-
-			Map<String, String> attributeStore = user.getAttributeStore();
-			attributeStore.clear();
-			for (Entry<String, List<Object>> entry : attributeMap.entrySet()) {
-				attributeStore.put(entry.getKey(), attrHelper.attributeListToString(entry.getValue()));
-			}
-
-			IncomingAttributeSetEntity incomingAttributeSet = incomingAttributeHandler.createOrUpdateAttributes(user, attributeMap);
-			incomingAttributeHandler.processIncomingAttributeSet(incomingAttributeSet);
-			
-			identityUpdater.updateIdentity(user);
-
-			if (appConfig.getConfigValue("create_missing_eppn_scope") != null) {
-				if (user.getEppn() == null) {
-					String scope = appConfig.getConfigValue("create_missing_eppn_scope");
-					user.setEppn(user.getIdentity().getGeneratedLocalUsername() + "@" + scope);
-					changed = true;
-				}
-			}
-		}
-
-		for (ServiceEntity delayedService : delayedRegisterList) {
-			try {
-				registrator.registerUser(user, delayedService, "user-" + user.getId(), false);
-			} catch (RegisterException e) {
-				logger.warn("Parent registration didn't work out like it should", e);
-			}
-		}
-
-		changed |= postUpdateUser(user, attributeMap, user.getIdp().getGenericStore(), executor, service, debugLog,
-				lastLoginHost);
-
-		user.setLastUpdate(new Date());
-		user.setLastFailedUpdate(null);
-		user.setScheduledUpdate(getNextScheduledUpdate());
-
-		if (changed) {
-			fireUserChangeEvent(user, auditor.getActualExecutor(), auditor);
-		}
-
-		auditor.setUser(user);
-		auditor.finishAuditTrail();
-		auditor.commitAuditTrail();
-
-		if (debugLog != null) {
-			AuditUserUpdateEntity audit = auditor.getAudit();
-			debugLog.append("\n\nPrinting audit from user update process:\n\nName: ").append(audit.getName())
-					.append("\nDetail: ").append(audit.getDetail()).append("\n");
-			for (AuditDetailEntity detail : audit.getAuditDetails()) {
-				debugLog.append(detail.getEndTime()).append(" | ").append(detail.getSubject()).append(" | ")
-						.append(detail.getObject()).append(" | ").append(detail.getAction()).append(" | ")
-						.append(detail.getLog()).append(" | ").append(detail.getAuditStatus()).append("\n");
-			}
-
-			if (audit.getAuditDetails().size() == 0) {
-				debugLog.append("Nothing seems to have changed.\n");
-			}
-		}
-
-		return user;
-	}
-
-	private Date getNextScheduledUpdate() {
-		Long futureMillis = 30L * 24L * 60L * 60L * 1000L;
-		if (appConfig.getConfigOptions().containsKey("update_schedule_future")) {
-			futureMillis = Long.decode(appConfig.getConfigValue("update_schedule_future"));
-		}
-		Integer futureMillisRandom = 6 * 60 * 60 * 1000;
-		if (appConfig.getConfigOptions().containsKey("update_schedule_future_random")) {
-			futureMillisRandom = Integer.decode(appConfig.getConfigValue("update_schedule_future_random"));
-		}
-		Random r = new Random();
-		return new Date(System.currentTimeMillis() + futureMillis + r.nextInt(futureMillisRandom));
-	}
-
-	public SamlUserEntity updateUser(SamlUserEntity user, Assertion assertion, String executor, ServiceEntity service,
-			StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-
-		if (assertion != null) {
-			samlAsserionDao.deleteAssertionForUser(user);
-
-			SamlAssertionEntity samlAssertionEntity = samlAsserionDao.createNew();
-			samlAssertionEntity.setUser(user);
-			samlAssertionEntity.setAssertionData(samlHelper.prettyPrint(assertion));
-			samlAssertionEntity.setValidUntil(new Date(System.currentTimeMillis() + (4L * 60L * 60L * 1000L)));
-			samlAssertionEntity = samlAsserionDao.persist(samlAssertionEntity);
-		}
-
-		Map<String, List<Object>> attributeMap = saml2AssertionService.extractAttributes(assertion);
-
-		if (debugLog != null) {
-			debugLog.append("Extracted attributes from Assertion:\n");
-			for (Entry<String, List<Object>> entry : attributeMap.entrySet()) {
-				debugLog.append(entry.getKey()).append(":\t").append(entry.getValue()).append("\n");
-			}
-		}
-
-		if (service != null)
-			return updateUser(user, attributeMap, executor, service, debugLog, lastLoginHost);
-		else
-			return updateUser(user, attributeMap, executor, debugLog, lastLoginHost);
-	}
-
-	public SamlUserEntity updateUser(SamlUserEntity user, Assertion assertion, String executor, String lastLoginHost)
-			throws UserUpdateException {
-
-		return updateUser(user, assertion, executor, null, null, lastLoginHost);
-	}
-
-	public SamlUserEntity updateUserFromIdp(SamlUserEntity user, String executor) throws UserUpdateException {
-		return updateUserFromIdp(user, null, executor, null);
-	}
-
-	public SamlUserEntity updateUserFromIdp(SamlUserEntity user, ServiceEntity service, String executor,
-			StringBuffer debugLog) throws UserUpdateException {
-
-		SamlSpConfigurationEntity spEntity = spDao.findByEntityId(user.getPersistentSpId());
-		SamlIdpMetadataEntity idpEntity = idpDao.findByEntityId(user.getIdp().getEntityId());
-
-		IdpCommunicationAuditor auditor = new IdpCommunicationAuditor(auditDao, auditDetailDao, appConfig);
-		auditor.setName("UpdateUserFromIdp");
-		auditor.setDetail("Call IDP " + idpEntity.getEntityId() + " from SP " + spEntity.getEntityId() + " for User "
-				+ user.getEppn());
-		auditor.setIdp(idpEntity);
-		auditor.setSpConfig(spEntity);
-		auditor.startAuditTrail(executor);
-
-		EntityDescriptor idpEntityDescriptor = samlHelper.unmarshal(idpEntity.getEntityDescriptor(),
-				EntityDescriptor.class, auditor);
-
-		Response samlResponse;
-		try {
-			/*
-			 * If something goes wrong here, communication with the idp probably failed
-			 */
-
-			samlResponse = attrQueryHelper.query(user, idpEntity, idpEntityDescriptor, spEntity, debugLog);
-
-			if (logger.isTraceEnabled())
-				logger.trace("{}", samlHelper.prettyPrint(samlResponse));
-
-			if (debugLog != null) {
-				debugLog.append("\nIncoming SAML Response:\n\n").append(samlHelper.prettyPrint(samlResponse))
-						.append("\n");
-			}
-
-		} catch (SOAPException e) {
-			/*
-			 * This exception is thrown if the certificate chain is incomplete e.g.
-			 */
-			handleException(user, e, idpEntity, auditor, debugLog);
-			throw new UserUpdateException(e);
-		} catch (MetadataException e) {
-			/*
-			 * is thrown if AttributeQuery location is missing in metadata, or something is
-			 * wrong with the sp certificate
-			 */
-			handleException(user, e, idpEntity, auditor, debugLog);
-			throw new UserUpdateException(e);
-		} catch (SecurityException e) {
-			handleException(user, e, idpEntity, auditor, debugLog);
-			throw new UserUpdateException(e);
-		} catch (Exception e) {
-			handleException(user, e, idpEntity, auditor, debugLog);
-			throw new UserUpdateException(e);
-		}
-
-		try {
-			/*
-			 * Don't check Assertion Signature, because we are contacting the IDP directly
-			 */
-			Assertion assertion;
-			try {
-				if (debugLog != null) {
-					debugLog.append("\nExtracting Assertion from SAML Response without signature check...\n");
-				}
-
-				assertion = saml2AssertionService.processSamlResponse(samlResponse, idpEntity, idpEntityDescriptor,
-						spEntity, false);
-
-				if (logger.isTraceEnabled())
-					logger.trace("{}", samlHelper.prettyPrint(assertion));
-
-			} catch (NoAssertionException e) {
-				if (user.getIdp() != null)
-					logger.warn("No assertion delivered for user {} from idp {}", user.getEppn(),
-							user.getIdp().getEntityId());
-				else
-					logger.warn("No assertion delivered for user {} from idp {}", user.getEppn());
-				assertion = null;
-			} catch (SamlUnknownPrincipalException e) {
-				if (user.getIdp() != null)
-					logger.warn("Unknown principal status for user {} from idp {}", user.getEppn(),
-							user.getIdp().getEntityId());
-				else
-					logger.warn("Unknown principal status  for user {}", user.getEppn());
-				assertion = null;
-			}
-
-			updateIdpStatus(SamlIdpMetadataEntityStatus.GOOD, idpEntity);
-
-			return updateUser(user, assertion, "attribute-query", service, debugLog, null);
-		} catch (DecryptionException e) {
-			handleException(user, e, idpEntity, auditor, debugLog);
-			throw new UserUpdateException(e);
-		} catch (IOException e) {
-			handleException(user, e, idpEntity, auditor, debugLog);
-			throw new UserUpdateException(e);
-		} catch (SamlAuthenticationException e) {
-			/*
-			 * Thrown if i.e. the AttributeQuery profile is not configured correctly
-			 */
-			handleException(user, e, idpEntity, auditor, debugLog);
-			throw new UserUpdateException(e);
-		}
-	}
-
-	protected void handleException(SamlUserEntity user, Exception e, SamlIdpMetadataEntity idpEntity, Auditor auditor,
-			StringBuffer debugLog) {
-		updateFail(user);
-		String message = e.getMessage();
-		if (e.getCause() != null)
-			message += " InnerCause: " + e.getCause().getMessage();
-		auditor.logAction(idpEntity.getEntityId(), "SAML ATTRIBUTE QUERY", user.getEppn(), message, AuditStatus.FAIL);
-		auditor.finishAuditTrail();
-		auditor.commitAuditTrail();
-
-		if (debugLog != null) {
-			debugLog.append("Attribute Query failed: ").append(e.getMessage());
-			if (e.getCause() != null)
-				debugLog.append("Cause: ").append(e.getCause().getMessage());
-			debugLog.append(logHelper.convertStacktrace(e));
-		}
-
-		updateIdpStatus(SamlIdpMetadataEntityStatus.FAULTY, idpEntity);
-	}
-
-	protected void updateIdpStatus(SamlIdpMetadataEntityStatus status, SamlIdpMetadataEntity idpEntity) {
-		if (!status.equals(idpEntity.getAqIdpStatus())) {
-			idpEntity.setAqIdpStatus(status);
-			idpEntity.setLastAqStatusChange(new Date());
-		}
-	}
-
-	protected void updateFail(SamlUserEntity user) {
-		user.setLastFailedUpdate(new Date());
-		user.setScheduledUpdate(getNextScheduledUpdate());
-		user = userDao.persist(user);
-	}
-
-	protected void fireUserChangeEvent(UserEntity user, String executor, Auditor auditor) {
-
-		UserEvent userEvent = new UserEvent(user, auditor.getAudit());
-
-		try {
-			eventSubmitter.submit(userEvent, EventType.USER_UPDATE, executor);
-		} catch (EventSubmitException e) {
-			logger.warn("Could not submit event", e);
-		}
-	}
-
-	public boolean updateUserNew(SamlUserEntity user, Map<String, List<Object>> attributeMap, String executor,
-			Auditor auditor, StringBuffer debugLog, String lastLoginHost) throws UserUpdateException {
-		boolean changed = false;
-
-		changed |= preUpdateUser(user, attributeMap, user.getIdp().getGenericStore(), executor, null, debugLog);
-		changed |= updateUserFromAttribute(user, attributeMap, auditor);
-		changed |= postUpdateUser(user, attributeMap, user.getIdp().getGenericStore(), executor, null, debugLog,
-				lastLoginHost);
-
-		return changed;
-	}
-
-	public boolean updateUserFromAttribute(SamlUserEntity user, Map<String, List<Object>> attributeMap, Auditor auditor)
-			throws UserUpdateException {
-		return updateUserFromAttribute(user, attributeMap, false, auditor);
-	}
-
-	public boolean updateUserFromAttribute(SamlUserEntity user, Map<String, List<Object>> attributeMap,
-			boolean withoutUidNumber, Auditor auditor) throws UserUpdateException {
-
-		boolean changed = false;
-
-		UserServiceHook completeOverrideHook = null;
-		Set<UserServiceHook> activeHooks = new HashSet<UserServiceHook>();
-
-		for (UserServiceHook hook : hookManager.getUserHooks()) {
-			if (hook.isResponsible(user, attributeMap)) {
-
-				hook.preUpdateUserFromAttribute(user, attributeMap, auditor);
-				activeHooks.add(hook);
-
-				if (hook.isCompleteOverride()) {
-					completeOverrideHook = hook;
-				}
-			}
-		}
-
-		if (completeOverrideHook == null) {
-			changed |= compareAndChangeProperty(user, "email", attributeMap.get("urn:oid:0.9.2342.19200300.100.1.3"),
-					auditor);
-			changed |= compareAndChangeProperty(user, "eppn", attributeMap.get("urn:oid:1.3.6.1.4.1.5923.1.1.1.6"),
-					auditor);
-			changed |= compareAndChangeProperty(user, "givenName", attributeMap.get("urn:oid:2.5.4.42"), auditor);
-			changed |= compareAndChangeProperty(user, "surName", attributeMap.get("urn:oid:2.5.4.4"), auditor);
-
-			List<String> emailList = attrHelper.attributeListToStringList(attributeMap,
-					"urn:oid:0.9.2342.19200300.100.1.3");
-			if (emailList != null && emailList.size() > 1) {
-
-				if (user.getEmailAddresses() == null) {
-					user.setEmailAddresses(new HashSet<String>());
-				}
-
-				for (int i = 1; i < emailList.size(); i++) {
-					user.getEmailAddresses().add(emailList.get(i));
-				}
-			}
-
-			if ((!withoutUidNumber) && (user.getUidNumber() == null)) {
-				user.setUidNumber(serialDao.nextUidNumber().intValue());
-				logger.info("Setting UID Number {} for user {}", user.getUidNumber(), user.getEppn());
-				auditor.logAction(user.getEppn(), "SET FIELD", "uidNumber", "" + user.getUidNumber(),
-						AuditStatus.SUCCESS);
-				changed = true;
-			}
-		} else {
-			logger.info("Overriding standard User Update Mechanism! Activator: {}",
-					completeOverrideHook.getClass().getName());
-		}
-
-		for (UserServiceHook hook : activeHooks) {
-			hook.postUpdateUserFromAttribute(user, attributeMap, auditor);
-		}
-
-		return changed;
-	}
-
-	private boolean compareAndChangeProperty(UserEntity user, String property, List<Object> objectValue,
-			Auditor auditor) {
-		String s = null;
-		String action = null;
-
-		// In case of a List (multiple SAML Values), take the first value
-		String value = attrHelper.getSingleStringFirst(objectValue);
-
-		try {
-			Object actualValue = PropertyUtils.getProperty(user, property);
-
-			if (actualValue != null && actualValue.equals(value)) {
-				// Value didn't change, do nothing
-				return false;
-			}
-
-			if (actualValue == null && value == null) {
-				// Value stayed null
-				return false;
-			}
-
-			if (actualValue == null) {
-				s = "null";
-				action = "SET FIELD";
-			} else {
-				s = actualValue.toString();
-				action = "UPDATE FIELD";
-			}
-
-			s = s + " -> " + value;
-			if (s.length() > 1017)
-				s = s.substring(0, 1017) + "...";
-
-			PropertyUtils.setProperty(user, property, value);
-
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.SUCCESS);
-		} catch (IllegalAccessException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		} catch (InvocationTargetException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		} catch (NoSuchMethodException e) {
-			logger.warn("This probably shouldn't happen: ", e);
-			auditor.logAction(user.getEppn(), action, property, s, AuditStatus.FAIL);
-		}
-
-		return true;
-	}
-
-	protected void changeUserStatus(UserEntity user, UserStatus toStatus, Auditor auditor) {
-		UserStatus fromStatus = user.getUserStatus();
-		user.setUserStatus(toStatus);
-		user.setLastStatusChange(new Date());
-
-		logger.debug("{}: change user status from {} to {}", user.getEppn(), fromStatus, toStatus);
-		auditor.logAction(user.getEppn(), "CHANGE STATUS", fromStatus + " -> " + toStatus,
-				"Change status " + fromStatus + " -> " + toStatus, AuditStatus.SUCCESS);
-	}
-
-	protected void changeRegistryStatus(RegistryEntity registry, RegistryStatus toStatus, String statusMessage,
-			Auditor parentAuditor) {
-		RegistryStatus fromStatus = registry.getRegistryStatus();
-		registry.setRegistryStatus(toStatus);
-		registry.setStatusMessage(statusMessage);
-		registry.setLastStatusChange(new Date());
-
-		logger.debug("{} {} {}: change registry status from {} to {}", new Object[] { registry.getUser().getEppn(),
-				registry.getService().getShortName(), registry.getId(), fromStatus, toStatus });
-		RegistryAuditor registryAuditor = new RegistryAuditor(auditDao, auditDetailDao, appConfig);
-		registryAuditor.setParent(parentAuditor);
-		registryAuditor.startAuditTrail(parentAuditor.getActualExecutor());
-		registryAuditor.setName(getClass().getName() + "-UserUpdate-Registry-Audit");
-		registryAuditor.setDetail("Update registry " + registry.getId() + " for user " + registry.getUser().getEppn());
-		registryAuditor.setRegistry(registry);
-		registryAuditor.logAction(registry.getUser().getEppn(), "CHANGE STATUS", "registry-" + registry.getId(),
-				"Change status " + fromStatus + " -> " + toStatus, AuditStatus.SUCCESS);
-		registryAuditor.finishAuditTrail();
-	}
-}
+package edu.kit.scc.webreg.service.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import edu.kit.scc.webreg.entity.ServiceEntity;
+import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.exc.UserUpdateException;
+
+public interface UserUpdater<T extends UserEntity> {
+
+	public T updateUser(T user, Map<String, List<Object>> attributeMap, String executor, StringBuffer debugLog, String lastLoginHost)
+			throws UserUpdateException;
+
+	public T updateUser(T user, Map<String, List<Object>> attributeMap, String executor, ServiceEntity service, StringBuffer debugLog, String lastLoginHost)
+			throws UserUpdateException;
+
+	public T updateUserFromHomeOrg(T user, ServiceEntity service, String executor,
+			StringBuffer debugLog) throws UserUpdateException;
+
+	public T expireUser(T user, String executor);
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/user/UserLifecycleManager.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/user/UserLifecycleManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a4d4a774277e0ed1b9cd6105b8090f93c82442d
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/user/UserLifecycleManager.java
@@ -0,0 +1,106 @@
+package edu.kit.scc.webreg.service.user;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+
+import edu.kit.scc.regapp.mail.impl.TemplateMailSender;
+import edu.kit.scc.webreg.dao.UserDao;
+import edu.kit.scc.webreg.dao.audit.AuditDetailDao;
+import edu.kit.scc.webreg.dao.audit.AuditEntryDao;
+import edu.kit.scc.webreg.entity.SamlUserEntity;
+import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.entity.oauth.OAuthUserEntity;
+import edu.kit.scc.webreg.entity.oidc.OidcUserEntity;
+import edu.kit.scc.webreg.event.EventSubmitter;
+import edu.kit.scc.webreg.exc.UserUpdateException;
+import edu.kit.scc.webreg.service.identity.IdentityScriptingEnv;
+import edu.kit.scc.webreg.service.impl.OAuthUserUpdater;
+import edu.kit.scc.webreg.service.impl.OidcUserUpdater;
+import edu.kit.scc.webreg.service.impl.SamlUserUpdater;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class UserLifecycleManager implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private AuditEntryDao auditDao;
+
+	@Inject
+	private AuditDetailDao auditDetailDao;
+
+	@Inject
+	private UserDao userDao;
+
+	@Inject
+	private IdentityScriptingEnv scriptingEnv;
+
+	@Inject
+	private TemplateMailSender mailService;
+
+	@Inject
+	private EventSubmitter eventSubmitter;
+
+	@Inject
+	private SamlUserUpdater userUpdater;
+
+	@Inject
+	private OidcUserUpdater oidcUserUpdater;
+
+	@Inject
+	private OAuthUserUpdater oauthUserUpdater;
+
+	public void sendUserExpiryWarning(UserEntity user, String emailTemplateName) {
+		logger.debug("Trying to send expiry warning to user {} to e-mail address {}. First updating...", user.getId(),
+				user.getIdentity().getPrimaryEmail());
+
+		try {
+			if (user instanceof SamlUserEntity) {
+				user = userUpdater.updateUserFromHomeOrg((SamlUserEntity) user, null, "user-expire-job", null);
+			} else if (user instanceof OidcUserEntity) {
+				user = oidcUserUpdater.updateUserFromHomeOrg((OidcUserEntity) user, null, "user-expire-job", null);
+			} else if (user instanceof OAuthUserEntity) {
+				user = oauthUserUpdater.updateUserFromHomeOrg((OAuthUserEntity) user, null, "user-expire-job", null);
+			}
+			
+			logger.info("Update didn't fail. Don't send expiry warning to user");
+		} catch (UserUpdateException e) {
+			logger.debug("Update failed, sending expiry warning to user {} to e-mail address {}", user.getId(),
+					user.getIdentity().getPrimaryEmail());
+			sendMail(user, emailTemplateName);
+			user.setExpireWarningSent(new Date());
+		}
+	}
+
+	public void expireUser(UserEntity user, String emailTemplateName) {
+		logger.debug("Trying to expire user {} with e-mail address {}", user.getId(),
+				user.getIdentity().getPrimaryEmail());
+
+		if (user instanceof SamlUserEntity) {
+			user = userUpdater.expireUser((SamlUserEntity) user, "user-expire-job");
+		} else if (user instanceof OidcUserEntity) {
+			user = oidcUserUpdater.expireUser((OidcUserEntity) user, "user-expire-job");
+		} else if (user instanceof OAuthUserEntity) {
+			user = oauthUserUpdater.expireUser((OAuthUserEntity) user, "user-expire-job");
+		}
+
+		sendMail(user, emailTemplateName);
+		user.setExpiredSent(new Date());
+	}
+
+	private void sendMail(UserEntity user, String emailTemplateName) {
+		Map<String, Object> context = new HashMap<String, Object>(2);
+		context.put("user", user);
+		context.put("identity", user.getIdentity());
+		mailService.sendMail(emailTemplateName, context, true);
+	}
+}
diff --git a/regapp-mail/src/main/java/edu/kit/scc/regapp/mail/impl/TemplateMailSender.java b/regapp-mail/src/main/java/edu/kit/scc/regapp/mail/impl/TemplateMailSender.java
index c36c21819d7119866f3ff08eee3ed6b7cd6f25b5..6d92364e1e74cff9f78e8cff9b3f9b11dd09697d 100644
--- a/regapp-mail/src/main/java/edu/kit/scc/regapp/mail/impl/TemplateMailSender.java
+++ b/regapp-mail/src/main/java/edu/kit/scc/regapp/mail/impl/TemplateMailSender.java
@@ -78,6 +78,7 @@ public class TemplateMailSender {
 						rendererContext.put("user", user);
 					}
 				}
+				
 			} else if (rendererContext.containsKey("user")) {
 				UserEntity user = userDao.fetch(((UserEntity) rendererContext.get("user")).getId());
 				rendererContext.putAll(userPrefsResolver.resolvePrefs(user.getIdentity()));
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcOpMetadataSingletonBean.java b/regapp-oidc/src/main/java/edu/kit/scc/regapp/oidc/tools/OidcOpMetadataSingletonBean.java
similarity index 96%
rename from bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcOpMetadataSingletonBean.java
rename to regapp-oidc/src/main/java/edu/kit/scc/regapp/oidc/tools/OidcOpMetadataSingletonBean.java
index c7e51c32207b711ede1ef8175bd9e9b6f2c73193..f36542d7eb24200893b71b15ad8a0035a815a9f3 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/client/OidcOpMetadataSingletonBean.java
+++ b/regapp-oidc/src/main/java/edu/kit/scc/regapp/oidc/tools/OidcOpMetadataSingletonBean.java
@@ -1,13 +1,10 @@
-package edu.kit.scc.webreg.service.oidc.client;
+package edu.kit.scc.regapp.oidc.tools;
 
 import java.io.IOException;
 import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
 
-import jakarta.ejb.Singleton;
-import jakarta.inject.Inject;
-
 import org.slf4j.Logger;
 
 import com.nimbusds.oauth2.sdk.ParseException;
@@ -17,8 +14,10 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderConfigurationRequest;
 import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
 import edu.kit.scc.webreg.entity.oidc.OidcRpConfigurationEntity;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
 
-@Singleton
+@ApplicationScoped
 public class OidcOpMetadataSingletonBean {
 
 	@Inject
diff --git a/regapp-tools/src/main/java/edu/kit/scc/webreg/tools/IdentityUserPrefsResolver.java b/regapp-tools/src/main/java/edu/kit/scc/webreg/tools/IdentityUserPrefsResolver.java
index ca6c9ffbd36b80fe9286ba57643272b6e94c5076..e54d7015b582a75ff3813fa1649b409bf3428bdf 100644
--- a/regapp-tools/src/main/java/edu/kit/scc/webreg/tools/IdentityUserPrefsResolver.java
+++ b/regapp-tools/src/main/java/edu/kit/scc/webreg/tools/IdentityUserPrefsResolver.java
@@ -50,6 +50,11 @@ public class IdentityUserPrefsResolver {
 			}
 		}
 		
+		if (identity.getPrimaryEmail() != null) {
+			// if a primary e-mail address is set, use it
+			prefsMap.put("email", identity.getPrimaryEmail().getEmailAddress());
+		}
+		
 		return prefsMap;
 	}