diff --git a/.deadcode-out b/.deadcode-out
index b5c043e8fd..600f348859 100644
--- a/.deadcode-out
+++ b/.deadcode-out
@@ -131,11 +131,13 @@ package "code.gitea.io/gitea/models/user"
 	func (ErrUserInactive).Unwrap
 	func IsErrExternalLoginUserAlreadyExist
 	func IsErrExternalLoginUserNotExist
+	func NewFederatedUser
 	func IsErrUserSettingIsNotExist
 	func GetUserAllSettings
 	func DeleteUserSetting
 	func GetUserEmailsByNames
 	func GetUserNamesByIDs
+	func DeleteFederatedUser
 
 package "code.gitea.io/gitea/modules/activitypub"
 	func (*Client).Post
@@ -169,16 +171,6 @@ package "code.gitea.io/gitea/modules/eventsource"
 
 package "code.gitea.io/gitea/modules/forgefed"
 	func NewForgeLike
-	func NewPersonID
-	func (PersonID).AsWebfinger
-	func (PersonID).AsLoginName
-	func (PersonID).HostSuffix
-	func (PersonID).Validate
-	func NewRepositoryID
-	func (RepositoryID).Validate
-	func (ForgePerson).MarshalJSON
-	func (*ForgePerson).UnmarshalJSON
-	func (ForgePerson).Validate
 	func GetItemByType
 	func JSONUnmarshalerFn
 	func NotEmpty
diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go
index fc5a460163..85229994b4 100644
--- a/models/forgejo_migrations/migrate.go
+++ b/models/forgejo_migrations/migrate.go
@@ -68,6 +68,10 @@ var migrations = []*Migration{
 	NewMigration("Remove Gitea-specific columns from the repository and badge tables", RemoveGiteaSpecificColumnsFromRepositoryAndBadge),
 	// v15 -> v16
 	NewMigration("Create the `federation_host` table", CreateFederationHostTable),
+	// v16 -> v17
+	NewMigration("Create the `federated_user` table", CreateFederatedUserTable),
+	// v17 -> v18
+	NewMigration("Add `normalized_federated_uri` column to `user` table", AddNormalizedFederatedURIToUser),
 }
 
 // GetCurrentDBVersion returns the current Forgejo database version.
diff --git a/models/forgejo_migrations/v16.go b/models/forgejo_migrations/v16.go
new file mode 100644
index 0000000000..f80bfc5268
--- /dev/null
+++ b/models/forgejo_migrations/v16.go
@@ -0,0 +1,17 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgejo_migrations //nolint:revive
+
+import "xorm.io/xorm"
+
+type FederatedUser struct {
+	ID               int64  `xorm:"pk autoincr"`
+	UserID           int64  `xorm:"NOT NULL"`
+	ExternalID       string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
+	FederationHostID int64  `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
+}
+
+func CreateFederatedUserTable(x *xorm.Engine) error {
+	return x.Sync(new(FederatedUser))
+}
diff --git a/models/forgejo_migrations/v17.go b/models/forgejo_migrations/v17.go
new file mode 100644
index 0000000000..d6e2983d00
--- /dev/null
+++ b/models/forgejo_migrations/v17.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgejo_migrations //nolint:revive
+
+import "xorm.io/xorm"
+
+func AddNormalizedFederatedURIToUser(x *xorm.Engine) error {
+	type User struct {
+		ID                     int64 `xorm:"pk autoincr"`
+		NormalizedFederatedURI string
+	}
+	return x.Sync(&User{})
+}
diff --git a/models/user/federated_user.go b/models/user/federated_user.go
new file mode 100644
index 0000000000..1fc42c3c32
--- /dev/null
+++ b/models/user/federated_user.go
@@ -0,0 +1,35 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"code.gitea.io/gitea/modules/validation"
+)
+
+type FederatedUser struct {
+	ID               int64  `xorm:"pk autoincr"`
+	UserID           int64  `xorm:"NOT NULL"`
+	ExternalID       string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
+	FederationHostID int64  `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
+}
+
+func NewFederatedUser(userID int64, externalID string, federationHostID int64) (FederatedUser, error) {
+	result := FederatedUser{
+		UserID:           userID,
+		ExternalID:       externalID,
+		FederationHostID: federationHostID,
+	}
+	if valid, err := validation.IsValid(result); !valid {
+		return FederatedUser{}, err
+	}
+	return result, nil
+}
+
+func (user FederatedUser) Validate() []string {
+	var result []string
+	result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
+	result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...)
+	result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...)
+	return result
+}
diff --git a/models/user/federated_user_test.go b/models/user/federated_user_test.go
new file mode 100644
index 0000000000..6a2112666f
--- /dev/null
+++ b/models/user/federated_user_test.go
@@ -0,0 +1,29 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/modules/validation"
+)
+
+func Test_FederatedUserValidation(t *testing.T) {
+	sut := FederatedUser{
+		UserID:           12,
+		ExternalID:       "12",
+		FederationHostID: 1,
+	}
+	if res, err := validation.IsValid(sut); !res {
+		t.Errorf("sut should be valid but was %q", err)
+	}
+
+	sut = FederatedUser{
+		ExternalID:       "12",
+		FederationHostID: 1,
+	}
+	if res, _ := validation.IsValid(sut); res {
+		t.Errorf("sut should be invalid")
+	}
+}
diff --git a/models/user/user.go b/models/user/user.go
index 10c4915f5e..5844189e17 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -1,5 +1,6 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
 // Copyright 2019 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
 package user
@@ -131,6 +132,9 @@ type User struct {
 	AvatarEmail     string `xorm:"NOT NULL"`
 	UseCustomAvatar bool
 
+	// For federation
+	NormalizedFederatedURI string
+
 	// Counters
 	NumFollowers int
 	NumFollowing int `xorm:"NOT NULL DEFAULT 0"`
@@ -303,6 +307,11 @@ func (u *User) HTMLURL() string {
 	return setting.AppURL + url.PathEscape(u.Name)
 }
 
+// APActorID returns the IRI to the api endpoint of the user
+func (u *User) APActorID() string {
+	return fmt.Sprintf("%vapi/v1/activitypub/user-id/%v", setting.AppURL, url.PathEscape(fmt.Sprintf("%v", u.ID)))
+}
+
 // OrganisationLink returns the organization sub page link.
 func (u *User) OrganisationLink() string {
 	return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
@@ -834,6 +843,17 @@ func ValidateUser(u *User, cols ...string) error {
 	return nil
 }
 
+func (u User) Validate() []string {
+	var result []string
+	if err := ValidateUser(&u); err != nil {
+		result = append(result, err.Error())
+	}
+	if err := ValidateEmail(u.Email); err != nil {
+		result = append(result, err.Error())
+	}
+	return result
+}
+
 // UpdateUserCols update user according special columns
 func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
 	if err := ValidateUser(u, cols...); err != nil {
diff --git a/models/user/user_repository.go b/models/user/user_repository.go
new file mode 100644
index 0000000000..c06441b5c8
--- /dev/null
+++ b/models/user/user_repository.go
@@ -0,0 +1,83 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"context"
+	"fmt"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/optional"
+	"code.gitea.io/gitea/modules/validation"
+)
+
+func init() {
+	db.RegisterModel(new(FederatedUser))
+}
+
+func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error {
+	if res, err := validation.IsValid(user); !res {
+		return err
+	}
+	overwrite := CreateUserOverwriteOptions{
+		IsActive:     optional.Some(false),
+		IsRestricted: optional.Some(false),
+	}
+
+	// Begin transaction
+	ctx, committer, err := db.TxContext((ctx))
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err := CreateUser(ctx, user, &overwrite); err != nil {
+		return err
+	}
+
+	federatedUser.UserID = user.ID
+	if res, err := validation.IsValid(federatedUser); !res {
+		return err
+	}
+
+	_, err = db.GetEngine(ctx).Insert(federatedUser)
+	if err != nil {
+		return err
+	}
+
+	// Commit transaction
+	return committer.Commit()
+}
+
+func FindFederatedUser(ctx context.Context, externalID string,
+	federationHostID int64,
+) (*User, *FederatedUser, error) {
+	federatedUser := new(FederatedUser)
+	user := new(User)
+	has, err := db.GetEngine(ctx).Where("external_id=? and federation_host_id=?", externalID, federationHostID).Get(federatedUser)
+	if err != nil {
+		return nil, nil, err
+	} else if !has {
+		return nil, nil, nil
+	}
+	has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user)
+	if err != nil {
+		return nil, nil, err
+	} else if !has {
+		return nil, nil, fmt.Errorf("User %v for federated user is missing", federatedUser.UserID)
+	}
+
+	if res, err := validation.IsValid(*user); !res {
+		return nil, nil, err
+	}
+	if res, err := validation.IsValid(*federatedUser); !res {
+		return nil, nil, err
+	}
+	return user, federatedUser, nil
+}
+
+func DeleteFederatedUser(ctx context.Context, userID int64) error {
+	_, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID})
+	return err
+}
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 4bf8c71369..7457256017 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -1,4 +1,5 @@
 // Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
 package user_test
@@ -107,6 +108,15 @@ func TestGetAllUsers(t *testing.T) {
 	assert.False(t, found[user_model.UserTypeOrganization], users)
 }
 
+func TestAPActorID(t *testing.T) {
+	user := user_model.User{ID: 1}
+	url := user.APActorID()
+	expected := "https://try.gitea.io/api/v1/activitypub/user-id/1"
+	if url != expected {
+		t.Errorf("unexpected APActorID, expected: %q, actual: %q", expected, url)
+	}
+}
+
 func TestSearchUsers(t *testing.T) {
 	defer tests.AddFixtures("models/user/fixtures/")()
 	assert.NoError(t, unittest.PrepareTestDatabase())
diff --git a/routers/api/v1/activitypub/repository.go b/routers/api/v1/activitypub/repository.go
index a9e94f289a..bc6e7905a6 100644
--- a/routers/api/v1/activitypub/repository.go
+++ b/routers/api/v1/activitypub/repository.go
@@ -74,9 +74,6 @@ func RepositoryInbox(ctx *context.APIContext) {
 	form := web.GetForm(ctx)
 	httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID)
 	if err != nil {
-		log.Error("Status: %v", httpStatus)
-		log.Error("Title: %v", title)
-		log.Error("Error: %v", err)
 		ctx.Error(httpStatus, title, err)
 	}
 	ctx.Status(http.StatusNoContent)
diff --git a/services/federation/federation_service.go b/services/federation/federation_service.go
index 5aba8b38c5..0e7efa133a 100644
--- a/services/federation/federation_service.go
+++ b/services/federation/federation_service.go
@@ -7,13 +7,19 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"net/url"
+	"strings"
 
 	"code.gitea.io/gitea/models/forgefed"
 	"code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/activitypub"
+	"code.gitea.io/gitea/modules/auth/password"
 	fm "code.gitea.io/gitea/modules/forgefed"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/validation"
+
+	"github.com/google/uuid"
 )
 
 // ProcessLikeActivity receives a ForgeLike activity and does the following:
@@ -40,6 +46,37 @@ func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int
 	if !activity.IsNewer(federationHost.LatestActivity) {
 		return http.StatusNotAcceptable, "Activity out of order.", fmt.Errorf("Activity already processed")
 	}
+	actorID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName))
+	if err != nil {
+		return http.StatusNotAcceptable, "Invalid PersonID", err
+	}
+	log.Info("Actor accepted:%v", actorID)
+
+	// parse objectID (repository)
+	objectID, err := fm.NewRepositoryID(activity.Object.GetID().String(), string(forgefed.ForgejoSourceType))
+	if err != nil {
+		return http.StatusNotAcceptable, "Invalid objectId", err
+	}
+	if objectID.ID != fmt.Sprint(repositoryID) {
+		return http.StatusNotAcceptable, "Invalid objectId", err
+	}
+	log.Info("Object accepted:%v", objectID)
+
+	// Check if user already exists
+	user, _, err := user.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
+	if err != nil {
+		return http.StatusInternalServerError, "Searching for user failed", err
+	}
+	if user != nil {
+		log.Info("Found local federatedUser: %v", user)
+	} else {
+		user, _, err = CreateUserFromAP(ctx, actorID, federationHost.ID)
+		if err != nil {
+			return http.StatusInternalServerError, "Error creating federatedUser", err
+		}
+		log.Info("Created federatedUser from ap: %v", user)
+	}
+	log.Info("Got user:%v", user.Name)
 
 	return 0, "", nil
 }
@@ -96,3 +133,67 @@ func GetFederationHostForURI(ctx context.Context, actorURI string) (*forgefed.Fe
 	}
 	return federationHost, nil
 }
+
+func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
+	// ToDo: Do we get a publicKeyId from server, repo or owner or repo?
+	actionsUser := user.NewActionsUser()
+	client, err := activitypub.NewClient(ctx, actionsUser, "no idea where to get key material.")
+	if err != nil {
+		return nil, nil, err
+	}
+
+	body, err := client.GetBody(personID.AsURI())
+	if err != nil {
+		return nil, nil, err
+	}
+
+	person := fm.ForgePerson{}
+	err = person.UnmarshalJSON(body)
+	if err != nil {
+		return nil, nil, err
+	}
+	if res, err := validation.IsValid(person); !res {
+		return nil, nil, err
+	}
+	log.Info("Fetched valid person:%q", person)
+
+	localFqdn, err := url.ParseRequestURI(setting.AppURL)
+	if err != nil {
+		return nil, nil, err
+	}
+	email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname())
+	loginName := personID.AsLoginName()
+	name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix())
+	fullName := person.Name.String()
+	if len(person.Name) == 0 {
+		fullName = name
+	}
+	password, err := password.Generate(32)
+	if err != nil {
+		return nil, nil, err
+	}
+	newUser := user.User{
+		LowerName:                    strings.ToLower(name),
+		Name:                         name,
+		FullName:                     fullName,
+		Email:                        email,
+		EmailNotificationsPreference: "disabled",
+		Passwd:                       password,
+		MustChangePassword:           false,
+		LoginName:                    loginName,
+		Type:                         user.UserTypeRemoteUser,
+		IsAdmin:                      false,
+		NormalizedFederatedURI:       personID.AsURI(),
+	}
+	federatedUser := user.FederatedUser{
+		ExternalID:       personID.ID,
+		FederationHostID: federationHostID,
+	}
+	err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
+	if err != nil {
+		return nil, nil, err
+	}
+	log.Info("Created federatedUser:%q", federatedUser)
+
+	return &newUser, &federatedUser, nil
+}
diff --git a/tests/integration/api_activitypub_repository_test.go b/tests/integration/api_activitypub_repository_test.go
index 67b18dac58..203307a369 100644
--- a/tests/integration/api_activitypub_repository_test.go
+++ b/tests/integration/api_activitypub_repository_test.go
@@ -91,15 +91,15 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
 				`"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`)
 			fmt.Fprint(res, responseBody)
 		})
-	federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/2",
+	federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/15",
 		func(res http.ResponseWriter, req *http.Request) {
 			// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2
 			responseBody := fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],` +
-				`"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2","type":"Person",` +
+				`"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15","type":"Person",` +
 				`"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/1bb05d9a5f6675ed0272af9ea193063c"},` +
-				`"url":"https://federated-repo.prod.meissa.de/stargoose1","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2/inbox",` +
-				`"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2/outbox","preferredUsername":"stargoose1",` +
-				`"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2",` +
+				`"url":"https://federated-repo.prod.meissa.de/stargoose1","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15/inbox",` +
+				`"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15/outbox","preferredUsername":"stargoose1",` +
+				`"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15",` +
 				`"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n` +
 				`CPiwhAqX3REvVq8tM79a2CEqZn9ka6vuXoDgBg/sBf/BUWqf7orkjUXwk/U0Egjf\nk5jcurF4vqf1u+rlAHH37dvSBaDjNj6Qnj4OP12bjfaY/yvs7+jue/eNXFHjzN4E\n` +
 				`T2H4B/yeKTJ4UuAwTlLaNbZJul2baLlHelJPAsxiYaziVuV5P+IGWckY6RSerRaZ\nAkc4mmGGtjAyfN9aewe+lNVfwS7ElFx546PlLgdQgjmeSwLX8FWxbPE5A/PmaXCs\n` +
@@ -107,6 +107,22 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
 				`LXX5AQ1xQNtlssnVoUBqBrvZsX2jUUKUocvZqMGuE4hfAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`)
 			fmt.Fprint(res, responseBody)
 		})
+	federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/30",
+		func(res http.ResponseWriter, req *http.Request) {
+			// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/3
+			responseBody := fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],` +
+				`"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30","type":"Person",` +
+				`"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/9c03f03d1c1f13f21976a22489326fe1"},` +
+				`"url":"https://federated-repo.prod.meissa.de/stargoose2","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30/inbox",` +
+				`"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30/outbox","preferredUsername":"stargoose2",` +
+				`"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30",` +
+				`"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyv5NytsfqpWXSrwuk8a3\n0W1zE13QJioXb/e3opgN2CfKZkdm3hb+4+mGKoU/rCqegnL9/AO0Aw+R8fCHXx44\n` +
+				`iNkdVpdY8Dzq+tQ9IetPWbyVIBvSzGgvpqfS05JuVPsy8cBX9wByODjr5kq7k1/v\nY1G7E3uh0a/XJc+mZutwGC3gPgR93NSrqsvTPN4wdhCCu9uj02S8OBoKuSYaPkU+\n` +
+				`tZ4CEDpnclAOw/eNiH4x2irMvVtruEgtlTA5K2I4YJrmtGLidus47FCyc8/zEKUh\nAeiD8KWDvqsQgOhUwcQgRxAnYVCoMD9cnE+WFFRHTuQecNlmdNFs3Cr0yKcWjDde\n` +
+				`trvnehW7LfPveGb0tHRHPuVAJpncTOidUR5h/7pqMyvKHzuAHWomm9rEaGUxd/7a\nL1CFjAf39+QIEgu0Anj8mIc7CTiz+DQhDz+0jBOsQ0iDXc5GeBz7X9Xv4Jp966nq\n` +
+				`MUR0GQGXvfZQN9IqMO+WoUVy10Ddhns1EWGlA0x4fecnAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`)
+			fmt.Fprint(res, responseBody)
+		})
 	federatedRoutes.HandleFunc("/",
 		func(res http.ResponseWriter, req *http.Request) {
 			t.Errorf("Unhandled request: %q", req.URL.EscapedPath())
@@ -129,20 +145,64 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
 			"%s/api/v1/activitypub/repository-id/%v/inbox",
 			srv.URL, repositoryID)
 
-		activity := []byte(fmt.Sprintf(
+		timeNow := time.Now().UTC()
+
+		activity1 := []byte(fmt.Sprintf(
 			`{"type":"Like",`+
 				`"startTime":"%s",`+
-				`"actor":"%s/api/v1/activitypub/user-id/2",`+
+				`"actor":"%s/api/v1/activitypub/user-id/15",`+
 				`"object":"%s/api/v1/activitypub/repository-id/%v"}`,
-			time.Now().UTC().Format(time.RFC3339),
+			timeNow.Format(time.RFC3339),
 			federatedSrv.URL, srv.URL, repositoryID))
-		t.Logf("activity: %s", activity)
-		resp, err := c.Post(activity, repoInboxURL)
+		t.Logf("activity: %s", activity1)
+		resp, err := c.Post(activity1, repoInboxURL)
 
 		assert.NoError(t, err)
 		assert.Equal(t, http.StatusNoContent, resp.StatusCode)
 
-		unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"})
+		federationHost := unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"})
+		federatedUser := unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "15", FederationHostID: federationHost.ID})
+		unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID})
+
+		// A like activity by a different user of the same federated host.
+		activity2 := []byte(fmt.Sprintf(
+			`{"type":"Like",`+
+				`"startTime":"%s",`+
+				`"actor":"%s/api/v1/activitypub/user-id/30",`+
+				`"object":"%s/api/v1/activitypub/repository-id/%v"}`,
+			// Make sure this activity happens later then the one before
+			timeNow.Add(time.Second).Format(time.RFC3339),
+			federatedSrv.URL, srv.URL, repositoryID))
+		t.Logf("activity: %s", activity2)
+		resp, err = c.Post(activity2, repoInboxURL)
+
+		assert.NoError(t, err)
+		assert.Equal(t, http.StatusNoContent, resp.StatusCode)
+
+		federatedUser = unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "30", FederationHostID: federationHost.ID})
+		unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID})
+
+		// The same user sends another like activity
+		otherRepositoryID := 3
+		otherRepoInboxURL := fmt.Sprintf(
+			"%s/api/v1/activitypub/repository-id/%v/inbox",
+			srv.URL, otherRepositoryID)
+		activity3 := []byte(fmt.Sprintf(
+			`{"type":"Like",`+
+				`"startTime":"%s",`+
+				`"actor":"%s/api/v1/activitypub/user-id/30",`+
+				`"object":"%s/api/v1/activitypub/repository-id/%v"}`,
+			// Make sure this activity happens later then the ones before
+			timeNow.Add(time.Second*2).Format(time.RFC3339),
+			federatedSrv.URL, srv.URL, otherRepositoryID))
+		t.Logf("activity: %s", activity3)
+		resp, err = c.Post(activity3, otherRepoInboxURL)
+
+		assert.NoError(t, err)
+		assert.Equal(t, http.StatusNoContent, resp.StatusCode)
+
+		federatedUser = unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "30", FederationHostID: federationHost.ID})
+		unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID})
 	})
 }