Repository: owncloud/ocis
Pull request #5955 (2023-03-29 11:39:29)

Add keycloak package.
This PR adds the keycloak package, which contains the following:

* More genericised functions for the invitations backend.
* User query functions.
* PII query functions.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af42e82a010..2a4ce553fdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,6 @@ The following sections list the changes for unreleased.
 * Bugfix - Return 425 on Thumbnails: [#5300](https://github.com/owncloud/ocis/pull/5300)
 * Bugfix - Disassociate users from deleted school: [#5343](https://github.com/owncloud/ocis/pull/5343)
 * Bugfix - Fix default role assignment for demo users: [#3432](https://github.com/owncloud/ocis/issues/3432)
-* Bugfix - Fix so that PATCH requests for groups actually updates the group name: [#5949](https://github.com/owncloud/ocis/pull/5949)
 * Bugfix - Fix Postprocessing events: [#5269](https://github.com/owncloud/ocis/pull/5269)
 * Bugfix - Fix Search tag indexing: [#5405](https://github.com/owncloud/ocis/pull/5405)
 * Bugfix - Populate expanded properties: [#5421](https://github.com/owncloud/ocis/pull/5421)
diff --git a/changelog/unreleased/fix-group-name-changes.md b/changelog/unreleased/fix-group-name-changes.md
deleted file mode 100644
index 93eced4cc9f..00000000000
--- a/changelog/unreleased/fix-group-name-changes.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Bugfix: Fix so that PATCH requests for groups actually updates the group name
-
-https://github.com/owncloud/ocis/pull/5949
-
diff --git a/ocis-pkg/keycloak/client.go b/ocis-pkg/keycloak/client.go
new file mode 100644
index 00000000000..8f39dda5385
--- /dev/null
+++ b/ocis-pkg/keycloak/client.go
@@ -0,0 +1,211 @@
+// Package keycloak is a package for keycloak utility functions.
+package keycloak
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+
+	"github.com/Nerzal/gocloak/v13"
+	libregraph "github.com/owncloud/libre-graph-api-go"
+)
+
+// Some attribute constants.
+// TODO: Make these configurable in the future.
+const (
+	_idAttr       = "OWNCLOUD_ID"
+	_userTypeAttr = "OWNCLOUD_USER_TYPE"
+)
+
+// ConcreteClient represents a concrete implementation of a keycloak client
+type ConcreteClient struct {
+	keycloak     GoCloak
+	clientID     string
+	clientSecret string
+	realm        string
+	baseURL      string
+}
+
+// New instantiates a new keycloak.Backend with a default gocloak client.
+func New(
+	baseURL, clientID, clientSecret, realm string,
+	insecureSkipVerify bool,
+) *ConcreteClient {
+	gc := gocloak.NewClient(baseURL)
+	restyClient := gc.RestyClient()
+	restyClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: insecureSkipVerify}) //nolint:gosec
+	return NewWithClient(gc, baseURL, clientID, clientSecret, realm)
+}
+
+// NewWithClient instantiates a new keycloak.Backend with a custom
+func NewWithClient(
+	gocloakClient GoCloak,
+	baseURL, clientID, clientSecret, realm string,
+) *ConcreteClient {
+	return &ConcreteClient{
+		keycloak:     gocloakClient,
+		baseURL:      baseURL,
+		clientID:     clientID,
+		clientSecret: clientSecret,
+		realm:        realm,
+	}
+}
+
+// CreateUser creates a user from a libregraph user and returns its *keycloak* ID.
+// TODO: For now we only call this from the invitation service where all the attributes are set correctly.
+//
+//	For more wider use, do some sanity checking on the user instance.
+func (c *ConcreteClient) CreateUser(ctx context.Context, realm string, user *libregraph.User, userActions []UserAction) (string, error) {
+	token, err := c.getToken(ctx)
+	if err != nil {
+		return "", err
+	}
+
+	req := gocloak.User{
+		Email:     user.Mail,
+		Enabled:   user.AccountEnabled,
+		Username:  user.OnPremisesSamAccountName,
+		FirstName: user.GivenName,
+		LastName:  user.Surname,
+		Attributes: &map[string][]string{
+			_idAttr:       {user.GetId()},
+			_userTypeAttr: {user.GetUserType()},
+		},
+		RequiredActions: convertUserActions(userActions),
+	}
+	return c.keycloak.CreateUser(ctx, token.AccessToken, realm, req)
+}
+
+// SendActionsMail sends a mail to the user with userID instructing them to do the actions defined in userActions.
+func (c *ConcreteClient) SendActionsMail(ctx context.Context, realm, userID string, userActions []UserAction) error {
+	token, err := c.getToken(ctx)
+	if err != nil {
+		return err
+	}
+	params := gocloak.ExecuteActionsEmail{
+		UserID:  &userID,
+		Actions: convertUserActions(userActions),
+	}
+
+	return c.keycloak.ExecuteActionsEmail(ctx, token.AccessToken, realm, params)
+}
+
+// GetUserByEmail looks up a user by email.
+func (c *ConcreteClient) GetUserByEmail(ctx context.Context, realm, mail string) (*libregraph.User, error) {
+	token, err := c.getToken(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	users, err := c.keycloak.GetUsers(ctx, token.AccessToken, realm, gocloak.GetUsersParams{
+		Email: &mail,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	if len(users) == 0 {
+		return nil, fmt.Errorf("no users found with mail address %s", mail)
+	}
+
+	if len(users) > 1 {
+		return nil, fmt.Errorf("%d users found with mail address %s, expected 1", len(users), mail)
+	}
+
+	return c.keycloakUserToLibregraph(users[0]), nil
+}
+
+// GetPIIReport returns a structure with all the PII for the user.
+func (c *ConcreteClient) GetPIIReport(ctx context.Context, realm string, user *libregraph.User) (*PIIReport, error) {
+	u, err := c.GetUserByEmail(ctx, realm, *user.Mail)
+	if err != nil {
+		return nil, err
+	}
+
+	token, err := c.getToken(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	keycloakID, err := c.getKeyCloakID(u)
+	if err != nil {
+		return nil, err
+	}
+
+	creds, err := c.keycloak.GetCredentials(ctx, token.AccessToken, realm, keycloakID)
+	if err != nil {
+		return nil, err
+	}
+
+	return &PIIReport{
+		UserData:    u,
+		Credentials: creds,
+	}, nil
+}
+
+// getToken gets a fresh token for the request.
+// TODO: set a token on the struct and check if it's still valid before requesting a new one.
+func (c *ConcreteClient) getToken(ctx context.Context) (*gocloak.JWT, error) {
+	token, err := c.keycloak.LoginClient(ctx, c.clientID, c.clientSecret, c.realm)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get token: %w", err)
+	}
+
+	rRes, err := c.keycloak.RetrospectToken(ctx, token.AccessToken, c.clientID, c.clientSecret, c.realm)
+	if err != nil {
+		return nil, fmt.Errorf("failed to retrospect token: %w", err)
+	}
+
+	if !*rRes.Active {
+		return nil, fmt.Errorf("token is not active")
+	}
+
+	return token, nil
+}
+
+func (c *ConcreteClient) keycloakUserToLibregraph(u *gocloak.User) *libregraph.User {
+	attrs := *u.Attributes
+	ldapID := ""
+	ldapIDs, ok := attrs[_idAttr]
+	if ok {
+		ldapID = ldapIDs[0]
+	}
+
+	var userType *string
+	userTypes, ok := attrs[_userTypeAttr]
+	if ok {
+		userType = &userTypes[0]
+	}
+
+	return &libregraph.User{
+		Id:             &ldapID,
+		Mail:           u.Email,
+		GivenName:      u.FirstName,
+		Surname:        u.LastName,
+		AccountEnabled: u.Enabled,
+		UserType:       userType,
+		Identities: []libregraph.ObjectIdentity{
+			{
+				Issuer:           &c.baseURL,
+				IssuerAssignedId: u.ID,
+			},
+		},
+	}
+}
+
+func (c *ConcreteClient) getKeyCloakID(u *libregraph.User) (string, error) {
+	for _, i := range u.Identities {
+		if *i.Issuer == c.baseURL {
+			return *i.IssuerAssignedId, nil
+		}
+	}
+	return "", fmt.Errorf("could not find identity for issuer: %s", c.baseURL)
+}
+
+func convertUserActions(userActions []UserAction) *[]string {
+	stringActions := make([]string, len(userActions))
+	for i, a := range userActions {
+		stringActions[i] = userActionsToString[a]
+	}
+	return &stringActions
+}
diff --git a/ocis-pkg/keycloak/gocloak.go b/ocis-pkg/keycloak/gocloak.go
new file mode 100644
index 00000000000..c4a4de0cdac
--- /dev/null
+++ b/ocis-pkg/keycloak/gocloak.go
@@ -0,0 +1,19 @@
+package keycloak
+
+import (
+	"context"
+
+	"github.com/Nerzal/gocloak/v13"
+)
+
+// GoCloak represents the parts of gocloak.GoCloak that we use, mainly here for mockery.
+//
+//go:generate mockery --name=GoCloak
+type GoCloak interface {
+	CreateUser(ctx context.Context, token, realm string, user gocloak.User) (string, error)
+	GetUsers(ctx context.Context, token, realm string, params gocloak.GetUsersParams) ([]*gocloak.User, error)
+	ExecuteActionsEmail(ctx context.Context, token, realm string, params gocloak.ExecuteActionsEmail) error
+	LoginClient(ctx context.Context, clientID, clientSecret, realm string) (*gocloak.JWT, error)
+	RetrospectToken(ctx context.Context, accessToken, clientID, clientSecret, realm string) (*gocloak.IntroSpectTokenResult, error)
+	GetCredentials(ctx context.Context, accessToken, realm, userID string) ([]*gocloak.CredentialRepresentation, error)
+}
diff --git a/ocis-pkg/keycloak/types.go b/ocis-pkg/keycloak/types.go
new file mode 100644
index 00000000000..1f5ddfa6423
--- /dev/null
+++ b/ocis-pkg/keycloak/types.go
@@ -0,0 +1,39 @@
+package keycloak
+
+import (
+	"context"
+
+	"github.com/Nerzal/gocloak/v13"
+	libregraph "github.com/owncloud/libre-graph-api-go"
+)
+
+// UserAction defines a type for user actions
+type UserAction int8
+
+// An incomplete list of UserActions
+const (
+	// UserActionUpdatePassword sets it that the user needs to change their password.
+	UserActionUpdatePassword UserAction = iota
+	// UserActionVerifyEmail sets it that the user needs to verify their email address.
+	UserActionVerifyEmail
+)
+
+// A lookup table to translate user actions to their string equivalents
+var userActionsToString = map[UserAction]string{
+	UserActionUpdatePassword: "UPDATE_PASSWORD",
+	UserActionVerifyEmail:    "VERIFY_EMAIL",
+}
+
+// PIIReport is a structure of all the PersonalIdentifiableInformation contained in keycloak.
+type PIIReport struct {
+	UserData    *libregraph.User
+	Credentials []*gocloak.CredentialRepresentation
+}
+
+// Client represents a keycloak client.
+type Client interface {
+	CreateUser(ctx context.Context, realm string, user *libregraph.User, userActions []UserAction) (string, error)
+	SendActionsMail(ctx context.Context, realm, userID string, userActions []UserAction) error
+	GetUserByEmail(ctx context.Context, realm, email string) (*libregraph.User, error)
+	GetPIIReport(ctx context.Context, realm string, user *libregraph.User) (*PIIReport, error)
+}
diff --git a/services/graph/pkg/identity/backend.go b/services/graph/pkg/identity/backend.go
index 1ee6b108243..903dfc65b21 100644
--- a/services/graph/pkg/identity/backend.go
+++ b/services/graph/pkg/identity/backend.go
@@ -33,8 +33,6 @@ type Backend interface {
 	CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error)
 	// DeleteGroup deletes a given group, identified by id
 	DeleteGroup(ctx context.Context, id string) error
-	// UpdateGroupName updates the group name
-	UpdateGroupName(ctx context.Context, groupID string, groupName string) error
 	GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.Group, error)
 	GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error)
 	// GetGroupMembers list all members of a group
diff --git a/services/graph/pkg/identity/cs3.go b/services/graph/pkg/identity/cs3.go
index 1f03170d127..331ea1574f0 100644
--- a/services/graph/pkg/identity/cs3.go
+++ b/services/graph/pkg/identity/cs3.go
@@ -192,11 +192,6 @@ func (i *CS3) DeleteGroup(ctx context.Context, id string) error {
 	return errorcode.New(errorcode.NotSupported, "not implemented")
 }
 
-// UpdateGroupName implements the Backend Interface. It's currently not supported for the CS3 backend
-func (i *CS3) UpdateGroupName(ctx context.Context, groupID string, groupName string) error {
-	return errorcode.New(errorcode.NotSupported, "not implemented")
-}
-
 // GetGroupMembers implements the Backend Interface. It's currently not supported for the CS3 backend
 func (i *CS3) GetGroupMembers(ctx context.Context, groupID string, _ *godata.GoDataRequest) ([]*libregraph.User, error) {
 	return nil, errorcode.New(errorcode.NotSupported, "not implemented")
diff --git a/services/graph/pkg/identity/ldap_group.go b/services/graph/pkg/identity/ldap_group.go
index 040ac9423d1..01a9d143ca1 100644
--- a/services/graph/pkg/identity/ldap_group.go
+++ b/services/graph/pkg/identity/ldap_group.go
@@ -2,7 +2,6 @@ package identity
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net/url"
 	"strings"
@@ -215,43 +214,6 @@ func (i *LDAP) DeleteGroup(ctx context.Context, id string) error {
 	return nil
 }
 
-// UpdateGroupName implements the Backend Interface.
-func (i *LDAP) UpdateGroupName(ctx context.Context, groupID string, groupName string) error {
-	logger := i.logger.SubloggerWithRequestID(ctx)
-	logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup")
-
-	ge, err := i.getLDAPGroupByID(groupID, true)
-	if err != nil {
-		return err
-	}
-
-	if ge.GetEqualFoldAttributeValue(i.groupAttributeMap.name) == groupName {
-		return nil
-	}
-
-	attributeTypeAndValue := ldap.AttributeTypeAndValue{
-		Type:  i.groupAttributeMap.name,
-		Value: groupName,
-	}
-	newDNString := attributeTypeAndValue.String()
-
-	logger.Debug().Str("originalDN", ge.DN).Str("newDN", newDNString).Msg("Modifying DN")
-	mrdn := ldap.NewModifyDNRequest(ge.DN, newDNString, true, "")
-
-	if err := i.conn.ModifyDN(mrdn); err != nil {
-		var lerr *ldap.Error
-		logger.Debug().Str("originalDN", ge.DN).Str("newDN", newDNString).Err(err).Msg("Failed to modify DN")
-		if errors.As(err, &lerr) {
-			if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
-				err = errorcode.New(errorcode.NameAlreadyExists, "Group name already in use")
-			}
-		}
-		return err
-	}
-
-	return nil
-}
-
 // AddMembersToGroup implements the Backend Interface for the LDAP backend.
 // Currently it is limited to adding Users as Group members. Adding other groups
 // as members is not yet implemented
diff --git a/services/graph/pkg/identity/mocks/backend.go b/services/graph/pkg/identity/mocks/backend.go
index 69e876002a8..89586889dfc 100644
--- a/services/graph/pkg/identity/mocks/backend.go
+++ b/services/graph/pkg/identity/mocks/backend.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.22.1. DO NOT EDIT.
+// Code generated by mockery v2.14.1. DO NOT EDIT.
 
 package mocks
 
@@ -38,10 +38,6 @@ func (_m *Backend) CreateGroup(ctx context.Context, group libregraph.Group) (*li
 	ret := _m.Called(ctx, group)
 
 	var r0 *libregraph.Group
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, libregraph.Group) (*libregraph.Group, error)); ok {
-		return rf(ctx, group)
-	}
 	if rf, ok := ret.Get(0).(func(context.Context, libregraph.Group) *libregraph.Group); ok {
 		r0 = rf(ctx, group)
 	} else {
@@ -50,6 +46,7 @@ func (_m *Backend) CreateGroup(ctx context.Context, group libregraph.Group) (*li
 		}
 	}
 
+	var r1 error
 	if rf, ok := ret.Get(1).(func(context.Context, libregraph.Group) error); ok {
 		r1 = rf(ctx, group)
 	} else {
@@ -64,10 +61,6 @@ func (_m *Backend) CreateUser(ctx context.Context, user libregraph.User) (*libre
 	ret := _m.Called(ctx, user)
 
 	var r0 *libregraph.User
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, libregraph.User) (*libregraph.User, error)); ok {
-		return rf(ctx, user)
-	}
 	if rf, ok := ret.Get(0).(func(context.Context, libregraph.User) *libregraph.User); ok {
 		r0 = rf(ctx, user)
 	} else {
@@ -76,6 +69,7 @@ func (_m *Backend) CreateUser(ctx context.Context, user libregraph.User) (*libre
 		}
 	}
 
+	var r1 error
 	if rf, ok := ret.Get(1).(func(context.Context, libregraph.User) error); ok {
 		r1 = rf(ctx, user)
 	} else {
@@ -118,10 +112,6 @@ func (_m *Backend) GetGroup(ctx context.Context, nameOrID string, queryParam url
 	ret := _m.Called(ctx, nameOrID, queryParam)
 
 	var r0 *libregraph.Group
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) (*libregraph.Group, error)); ok {
-		return rf(ctx, nameOrID, queryParam)
-	}
 	if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.Group); ok {
 		r0 = rf(ctx, nameOrID, queryParam)
 	} else {
@@ -130,6 +120,7 @@ func (_m *Backend) GetGroup(ctx context.Context, nameOrID string, queryParam url
 		}
 	}
 
+	var r1 error
 	if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
 		r1 = rf(ctx, nameOrID, queryParam)
 	} else {
@@ -144,10 +135,6 @@ func (_m *Backend) GetGroupMembers(ctx context.Context, id string, oreq *godata.
 	ret := _m.Called(ctx, id, oreq)
 
 	var r0 []*libregraph.User
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, *godata.GoDataRequest) ([]*libregraph.User, error)); ok {
-		return rf(ctx, id, oreq)
-	}
 	if rf, ok := ret.Get(0).(func(context.Context, string, *godata.GoDataRequest) []*libregraph.User); ok {
 		r0 = rf(ctx, id, oreq)
 	} else {
@@ -156,6 +143,7 @@ func (_m *Backend) GetGroupMembers(ctx context.Context, id string, oreq *godata.
 		}
 	}
 
+	var r1 error
 	if rf, ok := ret.Get(1).(func(context.Context, string, *godata.GoDataRequest) error); ok {
 		r1 = rf(ctx, id, oreq)
 	} else {
@@ -170,10 +158,6 @@ func (_m *Backend) GetGroups(ctx context.Context, queryParam url.Values) ([]*lib
 	ret := _m.Called(ctx, queryParam)
 
 	var r0 []*libregraph.Group
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, url.Values) ([]*libregraph.Group, error)); ok {
-		return rf(ctx, queryParam)
-	}
 	if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.Group); ok {
 		r0 = rf(ctx, queryParam)
 	} else {
@@ -182,6 +166,7 @@ func (_m *Backend) GetGroups(ctx context.Context, queryParam url.Values) ([]*lib
 		}
 	}
 
+	var r1 error
 	if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
 		r1 = rf(ctx, queryParam)
 	} else {
@@ -196,10 +181,6 @@ func (_m *Backend) GetUser(ctx context.Context, nameOrID string, oreq *godata.Go
 	ret := _m.Called(ctx, nameOrID, oreq)
 
 	var r0 *libregraph.User
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, *godata.GoDataRequest) (*libregraph.User, error)); ok {
-		return rf(ctx, nameOrID, oreq)
-	}
 	if rf, ok := ret.Get(0).(func(context.Context, string, *godata.GoDataRequest) *libregraph.User); ok {
 		r0 = rf(ctx, nameOrID, oreq)
 	} else {
@@ -208,6 +189,7 @@ func (_m *Backend) GetUser(ctx context.Context, nameOrID string, oreq *godata.Go
 		}
 	}
 
+	var r1 error
 	if rf, ok := ret.Get(1).(func(context.Context, string, *godata.GoDataRequest) error); ok {
 		r1 = rf(ctx, nameOrID, oreq)
 	} else {
@@ -222,10 +204,6 @@ func (_m *Backend) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]
 	ret := _m.Called(ctx, oreq)
 
 	var r0 []*libregraph.User
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *godata.GoDataRequest) ([]*libregraph.User, error)); ok {
-		return rf(ctx, oreq)
-	}
 	if rf, ok := ret.Get(0).(func(context.Context, *godata.GoDataRequest) []*libregraph.User); ok {
 		r0 = rf(ctx, oreq)
 	} else {
@@ -234,6 +212,7 @@ func (_m *Backend) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]
 		}
 	}
 
+	var r1 error
 	if rf, ok := ret.Get(1).(func(context.Context, *godata.GoDataRequest) error); ok {
 		r1 = rf(ctx, oreq)
 	} else {
@@ -257,29 +236,11 @@ func (_m *Backend) RemoveMemberFromGroup(ctx context.Context, groupID string, me
 	return r0
 }
 
-// UpdateGroupName provides a mock function with given fields: ctx, groupID, groupName
-func (_m *Backend) UpdateGroupName(ctx context.Context, groupID string, groupName string) error {
-	ret := _m.Called(ctx, groupID, groupName)
-
-	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
-		r0 = rf(ctx, groupID, groupName)
-	} else {
-		r0 = ret.Error(0)
-	}
-
-	return r0
-}
-
 // UpdateUser provides a mock function with given fields: ctx, nameOrID, user
 func (_m *Backend) UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error) {
 	ret := _m.Called(ctx, nameOrID, user)
 
 	var r0 *libregraph.User
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, libregraph.User) (*libregraph.User, error)); ok {
-		return rf(ctx, nameOrID, user)
-	}
 	if rf, ok := ret.Get(0).(func(context.Context, string, libregraph.User) *libregraph.User); ok {
 		r0 = rf(ctx, nameOrID, user)
 	} else {
@@ -288,6 +249,7 @@ func (_m *Backend) UpdateUser(ctx context.Context, nameOrID string, user libregr
 		}
 	}
 
+	var r1 error
 	if rf, ok := ret.Get(1).(func(context.Context, string, libregraph.User) error); ok {
 		r1 = rf(ctx, nameOrID, user)
 	} else {
diff --git a/services/graph/pkg/service/v0/groups.go b/services/graph/pkg/service/v0/groups.go
index 5fa92104860..7ddfd995e27 100644
--- a/services/graph/pkg/service/v0/groups.go
+++ b/services/graph/pkg/service/v0/groups.go
@@ -126,11 +126,6 @@ func (g Graph) PatchGroup(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if changes.HasDisplayName() {
-		groupName := changes.GetDisplayName()
-		err = g.identityBackend.UpdateGroupName(r.Context(), groupID, groupName)
-	}
-
 	if memberRefs, ok := changes.GetMembersodataBindOk(); ok {
 		// The spec defines a limit of 20 members maxium per Request
 		if len(memberRefs) > g.config.API.GroupMembersPatchLimit {
diff --git a/services/invitations/Makefile b/services/invitations/Makefile
index 717e7ff5d23..5e06194da78 100644
--- a/services/invitations/Makefile
+++ b/services/invitations/Makefile
@@ -25,7 +25,7 @@ include ../../.make/generate.mk
 
 .PHONY: ci-go-generate
 ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this target
-	$(MOCKERY) --dir pkg/backends/keycloak --output pkg/mocks --case underscore --name GoCloak
+	$(MOCKERY) --output pkg/mocks --case underscore --name Client  --srcpkg "github.com/owncloud/ocis/v2/ocis-pkg/keycloak"
 
 
 .PHONY: ci-node-generate
diff --git a/services/invitations/pkg/backends/keycloak/backend.go b/services/invitations/pkg/backends/keycloak/backend.go
index fa42f2c51d9..7d718de4d4d 100644
--- a/services/invitations/pkg/backends/keycloak/backend.go
+++ b/services/invitations/pkg/backends/keycloak/backend.go
@@ -3,31 +3,28 @@ package keycloak
 
 import (
 	"context"
-	"crypto/tls"
-	"fmt"
 
-	"github.com/Nerzal/gocloak/v13"
 	"github.com/google/uuid"
+	libregraph "github.com/owncloud/libre-graph-api-go"
+	"github.com/owncloud/ocis/v2/ocis-pkg/keycloak"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/invitations/pkg/invitations"
 )
 
 const (
-	idAttr       = "OWNCLOUD_ID"
-	userTypeAttr = "OWNCLOUD_USER_TYPE"
-	userTypeVal  = "Guest"
+	userType = "Guest"
 )
 
-var userRequiredActions = []string{"UPDATE_PASSWORD", "VERIFY_EMAIL"}
+var userRequiredActions = []keycloak.UserAction{
+	keycloak.UserActionUpdatePassword,
+	keycloak.UserActionVerifyEmail,
+}
 
 // Backend represents the keycloak backend.
 type Backend struct {
-	logger       log.Logger
-	client       GoCloak
-	clientID     string
-	clientSecret string
-	clientRealm  string
-	userRealm    string
+	logger    log.Logger
+	client    keycloak.Client
+	userRealm string
 }
 
 // New instantiates a new keycloak.Backend with a default gocloak client.
@@ -36,62 +33,50 @@ func New(
 	baseURL, clientID, clientSecret, clientRealm, userRealm string,
 	insecureSkipVerify bool,
 ) *Backend {
-	client := gocloak.NewClient(baseURL)
-	restyClient := client.RestyClient()
-	restyClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: insecureSkipVerify}) //nolint:gosec
-	return NewWithClient(logger, client, clientID, clientSecret, clientRealm, userRealm)
+	logger = log.Logger{
+		Logger: logger.With().
+			Str("invitationBackend", "keycloak").
+			Str("clientID", clientID).
+			Str("clientRealm", clientRealm).
+			Str("userRealm", userRealm).
+			Logger(),
+	}
+	client := keycloak.New(baseURL, clientID, clientSecret, clientRealm, insecureSkipVerify)
+	return NewWithClient(logger, client, userRealm)
 }
 
-// NewWithClient creates a new backend with the supplied GoCloak client.
+// NewWithClient creates a new backend with the supplied keycloak client.
 func NewWithClient(
 	logger log.Logger,
-	client GoCloak,
-	clientID, clientSecret, clientRealm, userRealm string,
+	client keycloak.Client,
+	userRealm string,
 ) *Backend {
 	return &Backend{
-		logger: log.Logger{
-			Logger: logger.With().
-				Str("invitationBackend", "keycloak").
-				Str("clientID", clientID).
-				Str("clientRealm", clientRealm).
-				Str("userRealm", userRealm).
-				Logger(),
-		},
-		client:       client,
-		clientID:     clientID,
-		clientSecret: clientSecret,
-		clientRealm:  clientRealm,
-		userRealm:    userRealm,
+		logger:    logger,
+		client:    client,
+		userRealm: userRealm,
 	}
 }
 
 // CreateUser creates a user in the keycloak backend.
 func (b Backend) CreateUser(ctx context.Context, invitation *invitations.Invitation) (string, error) {
-	token, err := b.getToken(ctx)
-	if err != nil {
-		return "", err
-	}
 	u := uuid.New()
 
 	b.logger.Info().
-		Str(idAttr, u.String()).
 		Str("email", invitation.InvitedUserEmailAddress).
 		Msg("Creating new user")
-	user := gocloak.User{
-		Email:    &invitation.InvitedUserEmailAddress,
-		Enabled:  gocloak.BoolP(true),
-		Username: &invitation.InvitedUserEmailAddress,
-		Attributes: &map[string][]string{
-			idAttr:       {u.String()},
-			userTypeAttr: {userTypeVal},
-		},
-		RequiredActions: &userRequiredActions,
+	user := &libregraph.User{
+		Mail:                     &invitation.InvitedUserEmailAddress,
+		AccountEnabled:           boolP(true),
+		OnPremisesSamAccountName: &invitation.InvitedUserEmailAddress,
+		Id:                       stringP(u.String()),
+		UserType:                 stringP(userType),
 	}
 
-	id, err := b.client.CreateUser(ctx, token.AccessToken, b.userRealm, user)
+	id, err := b.client.CreateUser(ctx, b.userRealm, user, userRequiredActions)
 	if err != nil {
 		b.logger.Error().
-			Str(idAttr, u.String()).
+			Str("userID", u.String()).
 			Str("email", invitation.InvitedUserEmailAddress).
 			Err(err).
 			Msg("Failed to create user")
@@ -106,35 +91,8 @@ func (b Backend) CanSendMail() bool { return true }
 
 // SendMail sends a mail to the user with details on how to reedeem the invitation.
 func (b Backend) SendMail(ctx context.Context, id string) error {
-	token, err := b.getToken(ctx)
-	if err != nil {
-		return err
-	}
-	params := gocloak.ExecuteActionsEmail{
-		UserID:  &id,
-		Actions: &userRequiredActions,
-	}
-	return b.client.ExecuteActionsEmail(ctx, token.AccessToken, b.userRealm, params)
+	return b.client.SendActionsMail(ctx, b.userRealm, id, userRequiredActions)
 }
 
-func (b Backend) getToken(ctx context.Context) (*gocloak.JWT, error) {
-	b.logger.Debug().Msg("Logging into keycloak")
-	token, err := b.client.LoginClient(ctx, b.clientID, b.clientSecret, b.clientRealm)
-	if err != nil {
-		b.logger.Error().Err(err).Msg("failed to get token")
-		return nil, fmt.Errorf("failed to get token: %w", err)
-	}
-
-	rRes, err := b.client.RetrospectToken(ctx, token.AccessToken, b.clientID, b.clientSecret, b.clientRealm)
-	if err != nil {
-		b.logger.Error().Err(err).Msg("failed to introspect token")
-		return nil, fmt.Errorf("failed to retrospect token: %w", err)
-	}
-
-	if !*rRes.Active {
-		b.logger.Error().Msg("token not active")
-		return nil, fmt.Errorf("token is not active")
-	}
-
-	return token, nil
-}
+func boolP(b bool) *bool       { return &b }
+func stringP(s string) *string { return &s }
diff --git a/services/invitations/pkg/mocks/client.go b/services/invitations/pkg/mocks/client.go
new file mode 100644
index 00000000000..eddaf9cec80
--- /dev/null
+++ b/services/invitations/pkg/mocks/client.go
@@ -0,0 +1,122 @@
+// Code generated by mockery v2.22.1. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	libregraph "github.com/owncloud/libre-graph-api-go"
+	keycloak "github.com/owncloud/ocis/v2/ocis-pkg/keycloak"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Client is an autogenerated mock type for the Client type
+type Client struct {
+	mock.Mock
+}
+
+// CreateUser provides a mock function with given fields: ctx, realm, user, userActions
+func (_m *Client) CreateUser(ctx context.Context, realm string, user *libregraph.User, userActions []keycloak.UserAction) (string, error) {
+	ret := _m.Called(ctx, realm, user, userActions)
+
+	var r0 string
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, *libregraph.User, []keycloak.UserAction) (string, error)); ok {
+		return rf(ctx, realm, user, userActions)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string, *libregraph.User, []keycloak.UserAction) string); ok {
+		r0 = rf(ctx, realm, user, userActions)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string, *libregraph.User, []keycloak.UserAction) error); ok {
+		r1 = rf(ctx, realm, user, userActions)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetPIIReport provides a mock function with given fields: ctx, realm, user
+func (_m *Client) GetPIIReport(ctx context.Context, realm string, user *libregraph.User) (*keycloak.PIIReport, error) {
+	ret := _m.Called(ctx, realm, user)
+
+	var r0 *keycloak.PIIReport
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, *libregraph.User) (*keycloak.PIIReport, error)); ok {
+		return rf(ctx, realm, user)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string, *libregraph.User) *keycloak.PIIReport); ok {
+		r0 = rf(ctx, realm, user)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*keycloak.PIIReport)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string, *libregraph.User) error); ok {
+		r1 = rf(ctx, realm, user)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetUserByEmail provides a mock function with given fields: ctx, realm, email
+func (_m *Client) GetUserByEmail(ctx context.Context, realm string, email string) (*libregraph.User, error) {
+	ret := _m.Called(ctx, realm, email)
+
+	var r0 *libregraph.User
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) (*libregraph.User, error)); ok {
+		return rf(ctx, realm, email)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) *libregraph.User); ok {
+		r0 = rf(ctx, realm, email)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*libregraph.User)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
+		r1 = rf(ctx, realm, email)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// SendActionsMail provides a mock function with given fields: ctx, realm, userID, userActions
+func (_m *Client) SendActionsMail(ctx context.Context, realm string, userID string, userActions []keycloak.UserAction) error {
+	ret := _m.Called(ctx, realm, userID, userActions)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, []keycloak.UserAction) error); ok {
+		r0 = rf(ctx, realm, userID, userActions)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewClient interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewClient(t mockConstructorTestingTNewClient) *Client {
+	mock := &Client{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/services/invitations/pkg/mocks/go_cloak.go b/services/invitations/pkg/mocks/go_cloak.go
deleted file mode 100644
index 6289046719c..00000000000
--- a/services/invitations/pkg/mocks/go_cloak.go
+++ /dev/null
@@ -1,112 +0,0 @@
-// Code generated by mockery v2.14.1. DO NOT EDIT.
-
-package mocks
-
-import (
-	context "context"
-
-	gocloak "github.com/Nerzal/gocloak/v13"
-
-	mock "github.com/stretchr/testify/mock"
-)
-
-// GoCloak is an autogenerated mock type for the GoCloak type
-type GoCloak struct {
-	mock.Mock
-}
-
-// CreateUser provides a mock function with given fields: ctx, token, realm, user
-func (_m *GoCloak) CreateUser(ctx context.Context, token string, realm string, user gocloak.User) (string, error) {
-	ret := _m.Called(ctx, token, realm, user)
-
-	var r0 string
-	if rf, ok := ret.Get(0).(func(context.Context, string, string, gocloak.User) string); ok {
-		r0 = rf(ctx, token, realm, user)
-	} else {
-		r0 = ret.Get(0).(string)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, string, string, gocloak.User) error); ok {
-		r1 = rf(ctx, token, realm, user)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-// ExecuteActionsEmail provides a mock function with given fields: ctx, token, realm, params
-func (_m *GoCloak) ExecuteActionsEmail(ctx context.Context, token string, realm string, params gocloak.ExecuteActionsEmail) error {
-	ret := _m.Called(ctx, token, realm, params)
-
-	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, string, gocloak.ExecuteActionsEmail) error); ok {
-		r0 = rf(ctx, token, realm, params)
-	} else {
-		r0 = ret.Error(0)
-	}
-
-	return r0
-}
-
-// LoginClient provides a mock function with given fields: ctx, clientID, clientSecret, realm
-func (_m *GoCloak) LoginClient(ctx context.Context, clientID string, clientSecret string, realm string) (*gocloak.JWT, error) {
-	ret := _m.Called(ctx, clientID, clientSecret, realm)
-
-	var r0 *gocloak.JWT
-	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) *gocloak.JWT); ok {
-		r0 = rf(ctx, clientID, clientSecret, realm)
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*gocloak.JWT)
-		}
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
-		r1 = rf(ctx, clientID, clientSecret, realm)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-// RetrospectToken provides a mock function with given fields: ctx, accessToken, clientID, clientSecret, realm
-func (_m *GoCloak) RetrospectToken(ctx context.Context, accessToken string, clientID string, clientSecret string, realm string) (*gocloak.IntroSpectTokenResult, error) {
-	ret := _m.Called(ctx, accessToken, clientID, clientSecret, realm)
-
-	var r0 *gocloak.IntroSpectTokenResult
-	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) *gocloak.IntroSpectTokenResult); ok {
-		r0 = rf(ctx, accessToken, clientID, clientSecret, realm)
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*gocloak.IntroSpectTokenResult)
-		}
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok {
-		r1 = rf(ctx, accessToken, clientID, clientSecret, realm)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-type mockConstructorTestingTNewGoCloak interface {
-	mock.TestingT
-	Cleanup(func())
-}
-
-// NewGoCloak creates a new instance of GoCloak. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
-func NewGoCloak(t mockConstructorTestingTNewGoCloak) *GoCloak {
-	mock := &GoCloak{}
-	mock.Mock.Test(t)
-
-	t.Cleanup(func() { mock.AssertExpectations(t) })
-
-	return mock
-}


Test patch:
diff --git a/services/graph/pkg/identity/ldap_group_test.go b/services/graph/pkg/identity/ldap_group_test.go
index 585b5e65af6..d375d1cef0a 100644
--- a/services/graph/pkg/identity/ldap_group_test.go
+++ b/services/graph/pkg/identity/ldap_group_test.go
@@ -8,7 +8,6 @@ import (
 
 	"github.com/go-ldap/ldap/v3"
 	"github.com/owncloud/ocis/v2/services/graph/mocks"
-	"github.com/stretchr/testify/assert"
 	"github.com/test-go/testify/mock"
 )
 
@@ -225,138 +224,3 @@ func TestGetGroups(t *testing.T) {
 		}
 	}
 }
-func TestUpdateGroupName(t *testing.T) {
-	groupDn := "cn=TheGroup,ou=groups,dc=owncloud,dc=com"
-
-	type args struct {
-		groupId   string
-		groupName string
-		newName   string
-	}
-
-	type mockInputs struct {
-		funcName string
-		args     []interface{}
-		returns  []interface{}
-	}
-
-	tests := []struct {
-		name      string
-		args      args
-		assertion assert.ErrorAssertionFunc
-		ldapMocks []mockInputs
-	}{
-		{
-			name: "Test with no name change",
-			args: args{
-				groupId: "some-uuid-string",
-				newName: "TheGroup",
-			},
-			assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
-				return assert.Nil(t, err, args...)
-			},
-			ldapMocks: []mockInputs{
-				{
-					funcName: "Search",
-					args: []interface{}{
-						ldap.NewSearchRequest(
-							"ou=groups,dc=test",
-							ldap.ScopeWholeSubtree,
-							ldap.NeverDerefAliases, 1, 0, false,
-							"(&(objectClass=groupOfNames)(entryUUID=some-uuid-string))",
-							[]string{"cn", "entryUUID", "member"},
-							nil,
-						),
-					},
-					returns: []interface{}{
-						&ldap.SearchResult{
-							Entries: []*ldap.Entry{
-								{
-									DN: groupDn,
-									Attributes: []*ldap.EntryAttribute{
-										{
-											Name:   "cn",
-											Values: []string{"TheGroup"},
-										},
-									},
-								},
-							},
-						},
-						nil,
-					},
-				},
-			},
-		},
-		{
-			name: "Test with name change",
-			args: args{
-				groupId: "some-uuid-string",
-				newName: "TheGroupWithShinyNewName",
-			},
-			assertion: func(t assert.TestingT, err error, args ...interface{}) bool {
-				return assert.Nil(t, err, args...)
-			},
-			ldapMocks: []mockInputs{
-				{
-					funcName: "Search",
-					args: []interface{}{
-						ldap.NewSearchRequest(
-							"ou=groups,dc=test",
-							ldap.ScopeWholeSubtree,
-							ldap.NeverDerefAliases, 1, 0, false,
-							"(&(objectClass=groupOfNames)(entryUUID=some-uuid-string))",
-							[]string{"cn", "entryUUID", "member"},
-							nil,
-						),
-					},
-					returns: []interface{}{
-						&ldap.SearchResult{
-							Entries: []*ldap.Entry{
-								{
-									DN: "cn=TheGroup,ou=groups,dc=owncloud,dc=com",
-									Attributes: []*ldap.EntryAttribute{
-										{
-											Name:   "cn",
-											Values: []string{"TheGroup"},
-										},
-									},
-								},
-							},
-						},
-						nil,
-					},
-				},
-				{
-					funcName: "ModifyDN",
-					args: []interface{}{
-						&ldap.ModifyDNRequest{
-							DN:           groupDn,
-							NewRDN:       "cn=TheGroupWithShinyNewName",
-							DeleteOldRDN: true,
-							NewSuperior:  "",
-							Controls:     []ldap.Control(nil),
-						},
-					},
-					returns: []interface{}{
-						nil,
-					},
-				},
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			lm := &mocks.Client{}
-			for _, mock := range tt.ldapMocks {
-				lm.On(mock.funcName, mock.args...).Return(mock.returns...)
-			}
-
-			ldapConfig := lconfig
-			i, _ := getMockedBackend(lm, ldapConfig, &logger)
-
-			err := i.UpdateGroupName(context.Background(), tt.args.groupId, tt.args.newName)
-			tt.assertion(t, err)
-		})
-	}
-}
diff --git a/services/graph/pkg/service/v0/groups_test.go b/services/graph/pkg/service/v0/groups_test.go
index 298e1922aac..16f7aa572e0 100644
--- a/services/graph/pkg/service/v0/groups_test.go
+++ b/services/graph/pkg/service/v0/groups_test.go
@@ -283,6 +283,7 @@ var _ = Describe("Groups", func() {
 
 			It("fails when the number of users is exceeded - spec says 20 max", func() {
 				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
 				updatedGroup.SetMembersodataBind([]string{
 					"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18",
 					"19", "20", "21",
@@ -305,6 +306,7 @@ var _ = Describe("Groups", func() {
 
 			It("succeeds when the number of users is over 20 but the limit is raised to 21", func() {
 				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
 				updatedGroup.SetMembersodataBind([]string{
 					"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18",
 					"19", "20", "21",
@@ -335,6 +337,7 @@ var _ = Describe("Groups", func() {
 
 			It("fails on invalid user refs", func() {
 				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
 				updatedGroup.SetMembersodataBind([]string{"invalid"})
 				updatedGroupJson, err := json.Marshal(updatedGroup)
 				Expect(err).ToNot(HaveOccurred())
@@ -350,6 +353,7 @@ var _ = Describe("Groups", func() {
 
 			It("fails when the adding non-users users", func() {
 				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
 				updatedGroup.SetMembersodataBind([]string{"/non-users/user1"})
 				updatedGroupJson, err := json.Marshal(updatedGroup)
 				Expect(err).ToNot(HaveOccurred())
@@ -367,6 +371,7 @@ var _ = Describe("Groups", func() {
 				identityBackend.On("AddMembersToGroup", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 
 				updatedGroup := libregraph.NewGroup()
+				updatedGroup.SetDisplayName("group1 updated")
 				updatedGroup.SetMembersodataBind([]string{"/users/user1"})
 				updatedGroupJson, err := json.Marshal(updatedGroup)
 				Expect(err).ToNot(HaveOccurred())
@@ -380,25 +385,6 @@ var _ = Describe("Groups", func() {
 				Expect(rr.Code).To(Equal(http.StatusNoContent))
 				identityBackend.AssertNumberOfCalls(GinkgoT(), "AddMembersToGroup", 1)
 			})
-
-			It("updates the group name", func() {
-				identityBackend.On("UpdateGroupName", mock.Anything, mock.Anything, mock.Anything).Return(nil)
-
-				updatedGroup := libregraph.NewGroup()
-				updatedGroup.SetDisplayName("group1 updated")
-				updatedGroupJson, err := json.Marshal(updatedGroup)
-				Expect(err).ToNot(HaveOccurred())
-
-				r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/groups", bytes.NewBuffer(updatedGroupJson))
-				rctx := chi.NewRouteContext()
-				rctx.URLParams.Add("groupID", *newGroup.Id)
-				r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-				svc.PatchGroup(rr, r)
-
-				Expect(rr.Code).To(Equal(http.StatusNoContent))
-				identityBackend.AssertNumberOfCalls(GinkgoT(), "UpdateGroupName", 1)
-			})
-
 		})
 	})
 
diff --git a/services/invitations/pkg/backends/keycloak/backend_test.go b/services/invitations/pkg/backends/keycloak/backend_test.go
index 6b8ef808058..019469b5292 100644
--- a/services/invitations/pkg/backends/keycloak/backend_test.go
+++ b/services/invitations/pkg/backends/keycloak/backend_test.go
@@ -7,7 +7,7 @@ import (
 	"context"
 	"testing"
 
-	"github.com/Nerzal/gocloak/v13"
+	kcpkg "github.com/owncloud/ocis/v2/ocis-pkg/keycloak"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/invitations/pkg/backends/keycloak"
 	"github.com/owncloud/ocis/v2/services/invitations/pkg/invitations"
@@ -34,11 +34,11 @@ func TestBackend_CreateUser(t *testing.T) {
 		returns  []interface{}
 	}
 	tests := []struct {
-		name          string
-		args          args
-		want          string
-		keycloakMocks []mockInputs
-		assertion     assert.ErrorAssertionFunc
+		name        string
+		args        args
+		want        string
+		clientMocks []mockInputs
+		assertion   assert.ErrorAssertionFunc
 	}{
 		{
 			name: "Test without diplay name",
@@ -48,46 +48,17 @@ func TestBackend_CreateUser(t *testing.T) {
 				},
 			},
 			want: "test-id",
-			keycloakMocks: []mockInputs{
-				{
-					funcName: "LoginClient",
-					args: []interface{}{
-						mock.Anything,
-						clientID,
-						clientSecret,
-						clientRealm,
-					},
-					returns: []interface{}{
-						&gocloak.JWT{
-							AccessToken: jwtToken,
-						},
-						nil,
-					},
-				},
-				{
-					funcName: "RetrospectToken",
-					args: []interface{}{
-						mock.Anything,
-						jwtToken,
-						clientID,
-						clientSecret,
-						clientRealm,
-					},
-					returns: []interface{}{
-						&gocloak.IntroSpectTokenResult{
-							Active: gocloak.BoolP(true),
-						},
-						nil,
-					},
-				},
+			clientMocks: []mockInputs{
 				{
 					funcName: "CreateUser",
 					args: []interface{}{
 						mock.Anything,
-						jwtToken,
 						userRealm,
 						mock.Anything, // can't match on the user because it generates a UUID internally.
-						// might be worth refactoring the UUID generation to outside of the func
+						[]kcpkg.UserAction{
+							kcpkg.UserActionUpdatePassword,
+							kcpkg.UserActionVerifyEmail,
+						},
 					},
 					returns: []interface{}{
 						"test-id",
@@ -103,11 +74,11 @@ func TestBackend_CreateUser(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			ctx := context.Background()
-			c := &mocks.GoCloak{}
-			for _, m := range tt.keycloakMocks {
+			c := &mocks.Client{}
+			for _, m := range tt.clientMocks {
 				c.On(m.funcName, m.args...).Return(m.returns...)
 			}
-			b := keycloak.NewWithClient(log.NopLogger(), c, clientID, clientSecret, clientRealm, userRealm)
+			b := keycloak.NewWithClient(log.NopLogger(), c, userRealm)
 			got, err := b.CreateUser(ctx, tt.args.invitation)
 			tt.assertion(t, err)
 			assert.Equal(t, tt.want, got)
@@ -125,57 +96,26 @@ func TestBackend_SendMail(t *testing.T) {
 		returns  []interface{}
 	}
 	tests := []struct {
-		name          string
-		args          args
-		keycloakMocks []mockInputs
-		assertion     assert.ErrorAssertionFunc
+		name        string
+		args        args
+		clientMocks []mockInputs
+		assertion   assert.ErrorAssertionFunc
 	}{
 		{
 			name: "Mail successfully sent",
 			args: args{
 				id: "test-id",
 			},
-			keycloakMocks: []mockInputs{
+			clientMocks: []mockInputs{
 				{
-					funcName: "LoginClient",
+					funcName: "SendActionsMail",
 					args: []interface{}{
 						mock.Anything,
-						clientID,
-						clientSecret,
-						clientRealm,
-					},
-					returns: []interface{}{
-						&gocloak.JWT{
-							AccessToken: jwtToken,
-						},
-						nil,
-					},
-				},
-				{
-					funcName: "RetrospectToken",
-					args: []interface{}{
-						mock.Anything,
-						jwtToken,
-						clientID,
-						clientSecret,
-						clientRealm,
-					},
-					returns: []interface{}{
-						&gocloak.IntroSpectTokenResult{
-							Active: gocloak.BoolP(true),
-						},
-						nil,
-					},
-				},
-				{
-					funcName: "ExecuteActionsEmail",
-					args: []interface{}{
-						mock.Anything,
-						jwtToken,
 						userRealm,
-						gocloak.ExecuteActionsEmail{
-							UserID:  gocloak.StringP("test-id"),
-							Actions: &[]string{"UPDATE_PASSWORD", "VERIFY_EMAIL"},
+						"test-id",
+						[]kcpkg.UserAction{
+							kcpkg.UserActionUpdatePassword,
+							kcpkg.UserActionVerifyEmail,
 						},
 					},
 					returns: []interface{}{
@@ -191,11 +131,11 @@ func TestBackend_SendMail(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			ctx := context.Background()
-			c := &mocks.GoCloak{}
-			for _, m := range tt.keycloakMocks {
+			c := &mocks.Client{}
+			for _, m := range tt.clientMocks {
 				c.On(m.funcName, m.args...).Return(m.returns...)
 			}
-			b := keycloak.NewWithClient(log.NopLogger(), c, clientID, clientSecret, clientRealm, userRealm)
+			b := keycloak.NewWithClient(log.NopLogger(), c, userRealm)
 			tt.assertion(t, b.SendMail(ctx, tt.args.id))
 		})
 	}
diff --git a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
index 65056b0654e..13bab45523e 100644
--- a/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
+++ b/tests/acceptance/expected-failures-localAPI-on-OCIS-storage.md
@@ -62,8 +62,13 @@ The expected failures in this file are from features in the owncloud/ocis repo.
 - [apiGraph/deleteGroup.feature:68](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/deleteGroup.feature#L68)
 
 #### [Updating group displayName request seems OK but group is not being renamed](https://github.com/owncloud/ocis/issues/5099)
+- [apiGraph/editGroup.feature:20](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L20)
+- [apiGraph/editGroup.feature:21](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L21)
+- [apiGraph/editGroup.feature:22](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L22)
 - [apiGraph/editGroup.feature:23](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L23)
+- [apiGraph/editGroup.feature:24](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L24)
 - [apiGraph/editGroup.feature:25](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L25)
+- [apiGraph/editGroup.feature:40](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiGraph/editGroup.feature#L40)
 
 #### [CORS headers are not identical with oC10 headers](https://github.com/owncloud/ocis/issues/5195)
 - [apiCors/cors.feature:25](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiCors/cors.feature#L25)