diff --git a/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs b/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs
index 28d12c0..ecd921d 100644
--- a/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs
+++ b/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs
@@ -6,5 +6,5 @@ namespace Phantom.Common.Data.Web.Users;
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record LogInSuccess(
 	[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo,
-	[property: MemoryPackOrder(1)] ImmutableArray<byte> Token
+	[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
 );
diff --git a/Common/Phantom.Common.Data.Web/Users/UserActionFailure.cs b/Common/Phantom.Common.Data.Web/Users/UserActionFailure.cs
new file mode 100644
index 0000000..3790d1e
--- /dev/null
+++ b/Common/Phantom.Common.Data.Web/Users/UserActionFailure.cs
@@ -0,0 +1,5 @@
+namespace Phantom.Common.Data.Web.Users;
+
+public enum UserActionFailure {
+	NotAuthorized
+}
diff --git a/Common/Phantom.Common.Data.Web/Users/UserInstanceActionFailure.cs b/Common/Phantom.Common.Data.Web/Users/UserInstanceActionFailure.cs
new file mode 100644
index 0000000..95789d5
--- /dev/null
+++ b/Common/Phantom.Common.Data.Web/Users/UserInstanceActionFailure.cs
@@ -0,0 +1,25 @@
+using MemoryPack;
+using Phantom.Common.Data.Replies;
+
+namespace Phantom.Common.Data.Web.Users;
+
+[MemoryPackable]
+[MemoryPackUnion(0, typeof(OfUserActionFailure))]
+[MemoryPackUnion(1, typeof(OfInstanceActionFailure))]
+public abstract partial record UserInstanceActionFailure {
+	internal UserInstanceActionFailure() {}
+	
+	public static implicit operator UserInstanceActionFailure(UserActionFailure failure) {
+		return new OfUserActionFailure(failure);
+	}
+	
+	public static implicit operator UserInstanceActionFailure(InstanceActionFailure failure) {
+		return new OfInstanceActionFailure(failure);
+	}
+}
+
+[MemoryPackable(GenerateType.VersionTolerant)]
+public sealed partial record OfUserActionFailure([property: MemoryPackOrder(0)] UserActionFailure Failure) : UserInstanceActionFailure;
+
+[MemoryPackable(GenerateType.VersionTolerant)]
+public sealed partial record OfInstanceActionFailure([property: MemoryPackOrder(0)] InstanceActionFailure Failure) : UserInstanceActionFailure;
diff --git a/Common/Phantom.Common.Data/Result.cs b/Common/Phantom.Common.Data/Result.cs
index 5fb88f8..507680e 100644
--- a/Common/Phantom.Common.Data/Result.cs
+++ b/Common/Phantom.Common.Data/Result.cs
@@ -1,5 +1,6 @@
 using System.Diagnostics.CodeAnalysis;
 using MemoryPack;
+using Phantom.Utils.Result;
 
 namespace Phantom.Common.Data;
 
@@ -33,10 +34,18 @@ public sealed partial class Result<TValue, TError> {
 		return hasValue && EqualityComparer<TValue>.Default.Equals(value, expectedValue);
 	}
 
-	public TOutput Map<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) {
+	public TOutput Into<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) {
 		return hasValue ? valueConverter(value!) : errorConverter(error!);
 	}
 
+	public Result<TValue, TNewError> MapError<TNewError>(Func<TError, TNewError> errorConverter) {
+		return hasValue ? value! : errorConverter(error!);
+	}
+
+	public Utils.Result.Result Variant() {
+		return hasValue ? new Ok<TValue>(Value) : new Err<TError>(Error);
+	}
+
 	public static implicit operator Result<TValue, TError>(TValue value) {
 		return new Result<TValue, TError>(hasValue: true, value, default);
 	}
diff --git a/Common/Phantom.Common.Messages.Web/ToController/ChangeUserRolesMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/ChangeUserRolesMessage.cs
index 20b267e..63aa15b 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/ChangeUserRolesMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/ChangeUserRolesMessage.cs
@@ -1,5 +1,6 @@
 using System.Collections.Immutable;
 using MemoryPack;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
@@ -7,8 +8,8 @@ namespace Phantom.Common.Messages.Web.ToController;
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record ChangeUserRolesMessage(
-	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
 	[property: MemoryPackOrder(1)] Guid SubjectUserGuid,
 	[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids,
 	[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids
-) : IMessageToController, ICanReply<ChangeUserRolesResult>;
+) : IMessageToController, ICanReply<Result<ChangeUserRolesResult, UserActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/CreateOrUpdateInstanceMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/CreateOrUpdateInstanceMessage.cs
index a35cd78..180e6ff 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/CreateOrUpdateInstanceMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/CreateOrUpdateInstanceMessage.cs
@@ -1,15 +1,16 @@
-using MemoryPack;
+using System.Collections.Immutable;
+using MemoryPack;
 using Phantom.Common.Data;
 using Phantom.Common.Data.Instance;
-using Phantom.Common.Data.Replies;
 using Phantom.Common.Data.Web.Instance;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
-namespace Phantom.Common.Messages.Web.ToController; 
+namespace Phantom.Common.Messages.Web.ToController;
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record CreateOrUpdateInstanceMessage(
-	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
 	[property: MemoryPackOrder(1)] Guid InstanceGuid,
 	[property: MemoryPackOrder(2)] InstanceConfiguration Configuration
-) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
+) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/CreateUserMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/CreateUserMessage.cs
index dbbdc86..1b0384a 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/CreateUserMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/CreateUserMessage.cs
@@ -1,4 +1,6 @@
-using MemoryPack;
+using System.Collections.Immutable;
+using MemoryPack;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
@@ -6,7 +8,7 @@ namespace Phantom.Common.Messages.Web.ToController;
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record CreateUserMessage(
-	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
 	[property: MemoryPackOrder(1)] string Username,
 	[property: MemoryPackOrder(2)] string Password
-) : IMessageToController, ICanReply<CreateUserResult>;
+) : IMessageToController, ICanReply<Result<CreateUserResult, UserActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/DeleteUserMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/DeleteUserMessage.cs
index 378904b..bc6bfca 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/DeleteUserMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/DeleteUserMessage.cs
@@ -1,4 +1,6 @@
-using MemoryPack;
+using System.Collections.Immutable;
+using MemoryPack;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
@@ -6,6 +8,6 @@ namespace Phantom.Common.Messages.Web.ToController;
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record DeleteUserMessage(
-	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
 	[property: MemoryPackOrder(1)] Guid SubjectUserGuid
-) : IMessageToController, ICanReply<DeleteUserResult>;
+) : IMessageToController, ICanReply<Result<DeleteUserResult, UserActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/GetAuditLogMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/GetAuditLogMessage.cs
index 2e09594..6135740 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/GetAuditLogMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/GetAuditLogMessage.cs
@@ -1,11 +1,14 @@
 using System.Collections.Immutable;
 using MemoryPack;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.AuditLog;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
 namespace Phantom.Common.Messages.Web.ToController;
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record GetAuditLogMessage(
-	[property: MemoryPackOrder(0)] int Count
-) : IMessageToController, ICanReply<ImmutableArray<AuditLogItem>>;
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
+	[property: MemoryPackOrder(1)] int Count
+) : IMessageToController, ICanReply<Result<ImmutableArray<AuditLogItem>, UserActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs b/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs
index 27ccec9..e68fee0 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs
@@ -9,5 +9,5 @@ namespace Phantom.Common.Messages.Web.ToController;
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record GetAuthenticatedUser(
 	[property: MemoryPackOrder(0)] Guid UserGuid,
-	[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken
+	[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
 ) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/GetEventLogMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/GetEventLogMessage.cs
index 58a1dbb..8f50f48 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/GetEventLogMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/GetEventLogMessage.cs
@@ -1,11 +1,14 @@
 using System.Collections.Immutable;
 using MemoryPack;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.EventLog;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
 namespace Phantom.Common.Messages.Web.ToController;
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record GetEventLogMessage(
-	[property: MemoryPackOrder(0)] int Count
-) : IMessageToController, ICanReply<ImmutableArray<EventLogItem>>;
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
+	[property: MemoryPackOrder(1)] int Count
+) : IMessageToController, ICanReply<Result<ImmutableArray<EventLogItem>, UserActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/LaunchInstanceMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/LaunchInstanceMessage.cs
index eebcdfa..aa706a6 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/LaunchInstanceMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/LaunchInstanceMessage.cs
@@ -1,13 +1,15 @@
-using MemoryPack;
+using System.Collections.Immutable;
+using MemoryPack;
 using Phantom.Common.Data;
 using Phantom.Common.Data.Replies;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
 namespace Phantom.Common.Messages.Web.ToController; 
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record LaunchInstanceMessage(
-	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
 	[property: MemoryPackOrder(1)] Guid AgentGuid,
 	[property: MemoryPackOrder(2)] Guid InstanceGuid
-) : IMessageToController, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
+) : IMessageToController, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/SendCommandToInstanceMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/SendCommandToInstanceMessage.cs
index 485cc6f..800a405 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/SendCommandToInstanceMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/SendCommandToInstanceMessage.cs
@@ -1,14 +1,16 @@
-using MemoryPack;
+using System.Collections.Immutable;
+using MemoryPack;
 using Phantom.Common.Data;
 using Phantom.Common.Data.Replies;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
 namespace Phantom.Common.Messages.Web.ToController; 
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record SendCommandToInstanceMessage(
-	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
 	[property: MemoryPackOrder(1)] Guid AgentGuid,
 	[property: MemoryPackOrder(2)] Guid InstanceGuid,
 	[property: MemoryPackOrder(3)] string Command
-) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
+) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/ToController/StopInstanceMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/StopInstanceMessage.cs
index 1f0a94a..2ec3a52 100644
--- a/Common/Phantom.Common.Messages.Web/ToController/StopInstanceMessage.cs
+++ b/Common/Phantom.Common.Messages.Web/ToController/StopInstanceMessage.cs
@@ -1,15 +1,17 @@
-using MemoryPack;
+using System.Collections.Immutable;
+using MemoryPack;
 using Phantom.Common.Data;
 using Phantom.Common.Data.Minecraft;
 using Phantom.Common.Data.Replies;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Actor;
 
 namespace Phantom.Common.Messages.Web.ToController; 
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record StopInstanceMessage(
-	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
+	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
 	[property: MemoryPackOrder(1)] Guid AgentGuid,
 	[property: MemoryPackOrder(2)] Guid InstanceGuid,
 	[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy
-) : IMessageToController, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
+) : IMessageToController, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>;
diff --git a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
index ba0b45c..b154994 100644
--- a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
+++ b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
@@ -28,20 +28,20 @@ public static class WebMessageRegistries {
 		ToController.Add<LogOutMessage>(3);
 		ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4);
 		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5);
-		ToController.Add<CreateUserMessage, CreateUserResult>(6);
-		ToController.Add<DeleteUserMessage, DeleteUserResult>(7);
+		ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(6);
+		ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(7);
 		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
 		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9);
 		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10);
-		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(11);
-		ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(12);
-		ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(13);
-		ToController.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(14);
-		ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(15);
+		ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(11);
+		ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(12);
+		ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(13);
+		ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(14);
+		ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(15);
 		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16);
 		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
-		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(18);
-		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(19);
+		ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18);
+		ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19);
 		ToController.Add<ReplyMessage>(127);
 		
 		ToWeb.Add<RegisterWebResultMessage>(0);
diff --git a/Controller/Phantom.Controller.Database/Repositories/PermissionRepository.cs b/Controller/Phantom.Controller.Database/Repositories/PermissionRepository.cs
new file mode 100644
index 0000000..463f755
--- /dev/null
+++ b/Controller/Phantom.Controller.Database/Repositories/PermissionRepository.cs
@@ -0,0 +1,26 @@
+using Microsoft.EntityFrameworkCore;
+using Phantom.Common.Data.Web.Users;
+using Phantom.Controller.Database.Entities;
+using Phantom.Utils.Collections;
+
+namespace Phantom.Controller.Database.Repositories;
+
+public sealed class PermissionRepository {
+	private readonly ILazyDbContext db;
+
+	public PermissionRepository(ILazyDbContext db) {
+		this.db = db;
+	}
+
+	public async Task<PermissionSet> GetAllUserPermissions(UserEntity user) {
+		var userPermissions = db.Ctx.UserPermissions
+		                        .Where(up => up.UserGuid == user.UserGuid)
+		                        .Select(static up => up.PermissionId);
+
+		var rolePermissions = db.Ctx.UserRoles
+		                        .Where(ur => ur.UserGuid == user.UserGuid)
+		                        .Join(db.Ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
+
+		return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
+	}
+}
diff --git a/Controller/Phantom.Controller.Services/Agents/AgentActor.cs b/Controller/Phantom.Controller.Services/Agents/AgentActor.cs
index 2bac9ad..6dd6760 100644
--- a/Controller/Phantom.Controller.Services/Agents/AgentActor.cs
+++ b/Controller/Phantom.Controller.Services/Agents/AgentActor.cs
@@ -10,12 +10,14 @@ using Phantom.Common.Data.Replies;
 using Phantom.Common.Data.Web.Agent;
 using Phantom.Common.Data.Web.Instance;
 using Phantom.Common.Data.Web.Minecraft;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Common.Messages.Agent;
 using Phantom.Common.Messages.Agent.ToAgent;
 using Phantom.Controller.Database;
 using Phantom.Controller.Database.Entities;
 using Phantom.Controller.Minecraft;
 using Phantom.Controller.Services.Instances;
+using Phantom.Controller.Services.Users.Sessions;
 using Phantom.Utils.Actor;
 using Phantom.Utils.Actor.Mailbox;
 using Phantom.Utils.Actor.Tasks;
@@ -32,7 +34,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 	private static readonly TimeSpan DisconnectionRecheckInterval = TimeSpan.FromSeconds(5);
 	private static readonly TimeSpan DisconnectionThreshold = TimeSpan.FromSeconds(12);
 
-	public readonly record struct Init(Guid AgentGuid, AgentConfiguration AgentConfiguration, ControllerState ControllerState, MinecraftVersions MinecraftVersions, IDbContextProvider DbProvider, CancellationToken CancellationToken);
+	public readonly record struct Init(Guid AgentGuid, AgentConfiguration AgentConfiguration, ControllerState ControllerState, MinecraftVersions MinecraftVersions, UserLoginManager UserLoginManager, IDbContextProvider DbProvider, CancellationToken CancellationToken);
 	
 	public static Props<ICommand> Factory(Init init) {
 		return Props<ICommand>.Create(() => new AgentActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name });
@@ -40,6 +42,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 
 	private readonly ControllerState controllerState;
 	private readonly MinecraftVersions minecraftVersions;
+	private readonly UserLoginManager userLoginManager;
 	private readonly IDbContextProvider dbProvider;
 	private readonly CancellationToken cancellationToken;
 	
@@ -76,6 +79,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 	private AgentActor(Init init) {
 		this.controllerState = init.ControllerState;
 		this.minecraftVersions = init.MinecraftVersions;
+		this.userLoginManager = init.UserLoginManager;
 		this.dbProvider = init.DbProvider;
 		this.cancellationToken = init.CancellationToken;
 		
@@ -94,11 +98,11 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 		Receive<NotifyIsAliveCommand>(NotifyIsAlive);
 		Receive<UpdateStatsCommand>(UpdateStats);
 		Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes);
-		ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance);
+		ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstance);
 		Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus);
-		ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
-		ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
-		ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand);
+		ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, UserInstanceActionFailure>>(LaunchInstance);
+		ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, UserInstanceActionFailure>>(StopInstance);
+		ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(SendMinecraftCommand);
 		Receive<ReceiveInstanceDataCommand>(ReceiveInstanceData);
 	}
 
@@ -146,13 +150,21 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 		}
 	}
 
-	private Task<Result<TReply, InstanceActionFailure>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
+	private async Task<Result<TReply, UserInstanceActionFailure>> RequestInstance<TCommand, TReply>(ImmutableArray<byte> authToken, Guid instanceGuid, Func<Guid, TCommand> commandFactoryFromLoggedInUserGuid) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
+		var loggedInUser = userLoginManager.GetLoggedInUser(authToken);
+		if (!loggedInUser.CheckPermission(Permission.ControlInstances)) {
+			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
+		}
+		
+		var command = commandFactoryFromLoggedInUserGuid(loggedInUser.Guid!.Value);
+		
 		if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) {
-			return instance.Request(command, cancellationToken);
+			var result = await instance.Request(command, cancellationToken);
+			return result.MapError(static error => (UserInstanceActionFailure) error);
 		}
 		else {
 			Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid);
-			return Task.FromResult<Result<TReply, InstanceActionFailure>>(InstanceActionFailure.InstanceDoesNotExist);
+			return (UserInstanceActionFailure) InstanceActionFailure.InstanceDoesNotExist;
 		}
 	}
 
@@ -183,15 +195,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 	
 	public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand;
 	
-	public sealed record CreateOrUpdateInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
+	public sealed record CreateOrUpdateInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>;
 	
 	public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand;
 
-	public sealed record LaunchInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
+	public sealed record LaunchInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>;
 	
-	public sealed record StopInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
+	public sealed record StopInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>;
 	
-	public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
+	public sealed record SendCommandToInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>;
 	
 	public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead;
 
@@ -280,25 +292,30 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 		controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes);
 	}
 	
-	private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) {
+	private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) {
+		var loggedInUser = userLoginManager.GetLoggedInUser(command.AuthToken);
+		if (!loggedInUser.CheckPermission(Permission.CreateInstances)) {
+			return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>((UserInstanceActionFailure) UserActionFailure.NotAuthorized);
+		}
+		
 		var instanceConfiguration = command.Configuration;
 
 		if (string.IsNullOrWhiteSpace(instanceConfiguration.InstanceName)) {
-			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty);
+			return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty);
 		}
 		
 		if (instanceConfiguration.MemoryAllocation <= RamAllocationUnits.Zero) {
-			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero);
+			return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero);
 		}
 		
 		return minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken)
-		                        .ContinueOnActor(CreateOrUpdateInstance1, command)
+		                        .ContinueOnActor(CreateOrUpdateInstance1, loggedInUser.Guid!.Value, command)
 		                        .Unwrap();
 	}
 
-	private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) {
+	private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, Guid loggedInUserGuid, CreateOrUpdateInstanceCommand command) {
 		if (serverExecutableInfo == null) {
-			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound);
+			return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound);
 		}
 		
 		var instanceConfiguration = command.Configuration;
@@ -308,13 +325,13 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 			instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration));
 		}
 		
-		var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.AuditLogUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
+		var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(loggedInUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
 		
 		return instanceActorRef.Request(configureInstanceCommand, cancellationToken)
 		                       .ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand);
 	}
 	
-	private Result<CreateOrUpdateInstanceResult, InstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) {
+	private Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) {
 		var instanceGuid = command.InstanceGuid;
 		var instanceName = command.Configuration.InstanceName;
 		var isCreating = command.IsCreatingInstance;
@@ -330,7 +347,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 		else {
 			string action = isCreating ? "adding" : "editing";
 			string relation = isCreating ? "to agent" : "in agent";
-			string reason = result.Map(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence);
+			string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence);
 			
 			Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, reason);
 			
@@ -342,16 +359,16 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
 		TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status));
 	}
 
-	private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
-		return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.AuditLogUserGuid));
+	private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
+		return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.AuthToken, command.InstanceGuid, static loggedInUserGuid => new InstanceActor.LaunchInstanceCommand(loggedInUserGuid));
 	}
 
-	private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) {
-		return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.AuditLogUserGuid, command.StopStrategy));
+	private Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(StopInstanceCommand command) {
+		return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.AuthToken, command.InstanceGuid, loggedInUserGuid => new InstanceActor.StopInstanceCommand(loggedInUserGuid, command.StopStrategy));
 	}
 
-	private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
-		return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.AuditLogUserGuid, command.Command));
+	private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
+		return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.AuthToken, command.InstanceGuid, loggedInUserGuid => new InstanceActor.SendCommandToInstanceCommand(loggedInUserGuid, command.Command));
 	}
 
 	private void ReceiveInstanceData(ReceiveInstanceDataCommand command) {
diff --git a/Controller/Phantom.Controller.Services/Agents/AgentManager.cs b/Controller/Phantom.Controller.Services/Agents/AgentManager.cs
index 874f6bf..168d32f 100644
--- a/Controller/Phantom.Controller.Services/Agents/AgentManager.cs
+++ b/Controller/Phantom.Controller.Services/Agents/AgentManager.cs
@@ -4,10 +4,12 @@ using Phantom.Common.Data;
 using Phantom.Common.Data.Agent;
 using Phantom.Common.Data.Replies;
 using Phantom.Common.Data.Web.Agent;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Common.Messages.Agent;
 using Phantom.Common.Messages.Agent.ToAgent;
 using Phantom.Controller.Database;
 using Phantom.Controller.Minecraft;
+using Phantom.Controller.Services.Users.Sessions;
 using Phantom.Utils.Actor;
 using Phantom.Utils.Logging;
 using Phantom.Utils.Rpc.Runtime;
@@ -22,17 +24,19 @@ sealed class AgentManager {
 	private readonly AuthToken authToken;
 	private readonly ControllerState controllerState;
 	private readonly MinecraftVersions minecraftVersions;
+	private readonly UserLoginManager userLoginManager;
 	private readonly IDbContextProvider dbProvider;
 	private readonly CancellationToken cancellationToken;
 	
 	private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new ();
 	private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
 	
-	public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
+	public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
 		this.actorSystem = actorSystem;
 		this.authToken = authToken;
 		this.controllerState = controllerState;
 		this.minecraftVersions = minecraftVersions;
+		this.userLoginManager = userLoginManager;
 		this.dbProvider = dbProvider;
 		this.cancellationToken = cancellationToken;
 		
@@ -40,7 +44,7 @@ sealed class AgentManager {
 	}
 
 	private ActorRef<AgentActor.ICommand> CreateAgentActor(Guid agentGuid, AgentConfiguration agentConfiguration) {
-		var init = new AgentActor.Init(agentGuid, agentConfiguration, controllerState, minecraftVersions, dbProvider, cancellationToken);
+		var init = new AgentActor.Init(agentGuid, agentConfiguration, controllerState, minecraftVersions, userLoginManager, dbProvider, cancellationToken);
 		var name = "Agent:" + agentGuid;
 		return actorSystem.ActorOf(AgentActor.Factory(init), name);
 	}
@@ -83,7 +87,7 @@ sealed class AgentManager {
 		}
 	}
 
-	public async Task<Result<TReply, InstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
-		return agentsByGuid.TryGetValue(agentGuid, out var agent) ? await agent.Request(command, cancellationToken) : InstanceActionFailure.AgentDoesNotExist;
+	public async Task<Result<TReply, UserInstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, UserInstanceActionFailure>> {
+		return agentsByGuid.TryGetValue(agentGuid, out var agent) ? await agent.Request(command, cancellationToken) : (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist;
 	}
 }
diff --git a/Controller/Phantom.Controller.Services/ControllerServices.cs b/Controller/Phantom.Controller.Services/ControllerServices.cs
index 6b70bb9..bc21293 100644
--- a/Controller/Phantom.Controller.Services/ControllerServices.cs
+++ b/Controller/Phantom.Controller.Services/ControllerServices.cs
@@ -11,6 +11,7 @@ using Phantom.Controller.Services.Events;
 using Phantom.Controller.Services.Instances;
 using Phantom.Controller.Services.Rpc;
 using Phantom.Controller.Services.Users;
+using Phantom.Controller.Services.Users.Sessions;
 using Phantom.Utils.Actor;
 using Phantom.Utils.Rpc.Runtime;
 using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController;
@@ -24,17 +25,18 @@ public sealed class ControllerServices : IDisposable {
 	private ControllerState ControllerState { get; }
 	private MinecraftVersions MinecraftVersions { get; }
 
-	private AgentManager AgentManager { get; }
-	private InstanceLogManager InstanceLogManager { get; }
-	private EventLogManager EventLogManager { get; }
-
+	private AuthenticatedUserCache AuthenticatedUserCache { get; }
 	private UserManager UserManager { get; }
 	private RoleManager RoleManager { get; }
-	private PermissionManager PermissionManager { get; }
-
 	private UserRoleManager UserRoleManager { get; }
 	private UserLoginManager UserLoginManager { get; }
+	private PermissionManager PermissionManager { get; }
+
+	private AgentManager AgentManager { get; }
+	private InstanceLogManager InstanceLogManager { get; }
+	
 	private AuditLogManager AuditLogManager { get; }
+	private EventLogManager EventLogManager { get; }
 
 	public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; }
 	public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; }
@@ -51,15 +53,16 @@ public sealed class ControllerServices : IDisposable {
 		this.ControllerState = new ControllerState();
 		this.MinecraftVersions = new MinecraftVersions();
 		
-		this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, dbProvider, cancellationToken);
+		this.AuthenticatedUserCache = new AuthenticatedUserCache();
+		this.UserManager = new UserManager(AuthenticatedUserCache, dbProvider);
+		this.RoleManager = new RoleManager(dbProvider);
+		this.UserRoleManager = new UserRoleManager(AuthenticatedUserCache, dbProvider);
+		this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, UserManager, dbProvider);
+		this.PermissionManager = new PermissionManager(dbProvider);
+		
+		this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
 		this.InstanceLogManager = new InstanceLogManager();
 		
-		this.UserManager = new UserManager(dbProvider);
-		this.RoleManager = new RoleManager(dbProvider);
-		this.PermissionManager = new PermissionManager(dbProvider);
-
-		this.UserRoleManager = new UserRoleManager(dbProvider);
-		this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider);
 		this.AuditLogManager = new AuditLogManager(dbProvider);
 		this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken);
 		
diff --git a/Controller/Phantom.Controller.Services/Events/EventLogManager.cs b/Controller/Phantom.Controller.Services/Events/EventLogManager.cs
index 7bad97e..2e129e9 100644
--- a/Controller/Phantom.Controller.Services/Events/EventLogManager.cs
+++ b/Controller/Phantom.Controller.Services/Events/EventLogManager.cs
@@ -1,8 +1,11 @@
 using System.Collections.Immutable;
 using Akka.Actor;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.EventLog;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Controller.Database;
 using Phantom.Controller.Database.Repositories;
+using Phantom.Controller.Services.Users.Sessions;
 using Phantom.Utils.Actor;
 
 namespace Phantom.Controller.Services.Events; 
@@ -22,7 +25,11 @@ sealed partial class EventLogManager {
 		databaseStorageActor.Tell(new EventLogDatabaseStorageActor.StoreEventCommand(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
 	}
 	
-	public async Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count) {
+	public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) {
+		if (!loggedInUser.CheckPermission(Permission.ViewEvents)) {
+			return UserActionFailure.NotAuthorized;
+		}
+		
 		await using var db = dbProvider.Lazy();
 		return await new EventLogRepository(db).GetMostRecentItems(count, cancellationToken);
 	}
diff --git a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
index 0ce7093..29d2cd8 100644
--- a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
+++ b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
@@ -15,6 +15,7 @@ using Phantom.Controller.Services.Agents;
 using Phantom.Controller.Services.Events;
 using Phantom.Controller.Services.Instances;
 using Phantom.Controller.Services.Users;
+using Phantom.Controller.Services.Users.Sessions;
 using Phantom.Utils.Actor;
 using Phantom.Utils.Rpc.Runtime;
 
@@ -67,27 +68,27 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 
 		var senderActorInit = new WebMessageDataUpdateSenderActor.Init(connection, controllerState, init.InstanceLogManager);
 		Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender");
-		
+
 		ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
 		Receive<UnregisterWebMessage>(HandleUnregisterWeb);
 		ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
 		Receive<LogOutMessage>(HandleLogOut);
 		ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
 		ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
-		ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
+		ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(HandleCreateUser);
 		ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
 		ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles);
 		ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles);
-		ReceiveAndReplyLater<ChangeUserRolesMessage, ChangeUserRolesResult>(HandleChangeUserRoles);
-		ReceiveAndReplyLater<DeleteUserMessage, DeleteUserResult>(HandleDeleteUser);
-		ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(HandleCreateOrUpdateInstance);
-		ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance);
-		ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance);
-		ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance);
-		ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions); 
+		ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(HandleChangeUserRoles);
+		ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(HandleDeleteUser);
+		ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(HandleCreateOrUpdateInstance);
+		ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(HandleLaunchInstance);
+		ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(HandleStopInstance);
+		ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(HandleSendCommandToInstance);
+		ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);
 		ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
-		ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog);
-		ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog);
+		ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog);
+		ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog);
 		Receive<ReplyMessage>(HandleReply);
 	}
 
@@ -108,15 +109,15 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 	}
 
 	private Optional<AuthenticatedUserInfo> GetAuthenticatedUser(GetAuthenticatedUser message) {
-		return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.SessionToken);
+		return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.AuthToken);
 	}
-    
+
 	private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
 		return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
 	}
 
-	private Task<CreateUserResult> HandleCreateUser(CreateUserMessage message) {
-		return userManager.Create(message.LoggedInUserGuid, message.Username, message.Password);
+	private Task<Result<CreateUserResult, UserActionFailure>> HandleCreateUser(CreateUserMessage message) {
+		return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password);
 	}
 
 	private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
@@ -131,28 +132,28 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 		return userRoleManager.GetUserRoles(message.UserGuids);
 	}
 
-	private Task<ChangeUserRolesResult> HandleChangeUserRoles(ChangeUserRolesMessage message) {
-		return userRoleManager.ChangeUserRoles(message.LoggedInUserGuid, message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
+	private Task<Result<ChangeUserRolesResult, UserActionFailure>> HandleChangeUserRoles(ChangeUserRolesMessage message) {
+		return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
 	}
 
-	private Task<DeleteUserResult> HandleDeleteUser(DeleteUserMessage message) {
-		return userManager.DeleteByGuid(message.LoggedInUserGuid, message.SubjectUserGuid);
+	private Task<Result<DeleteUserResult, UserActionFailure>> HandleDeleteUser(DeleteUserMessage message) {
+		return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid);
 	}
 
-	private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
-		return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Configuration));
+	private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
+		return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.AuthToken, message.InstanceGuid, message.Configuration));
 	}
 
-	private Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
-		return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid));
+	private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
+		return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.AuthToken, message.InstanceGuid));
 	}
 
-	private Task<Result<StopInstanceResult, InstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
-		return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.StopStrategy));
+	private Task<Result<StopInstanceResult, UserInstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
+		return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.AuthToken, message.InstanceGuid, message.StopStrategy));
 	}
 
-	private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
-		return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.Command));
+	private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
+		return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.AuthToken, message.InstanceGuid, message.Command));
 	}
 
 	private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
@@ -163,12 +164,12 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 		return controllerState.AgentJavaRuntimesByGuid;
 	}
 
-	private Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) {
-		return auditLogManager.GetMostRecentItems(message.Count);
+	private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> HandleGetAuditLog(GetAuditLogMessage message) {
+		return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
 	}
 
-	private Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage message) {
-		return eventLogManager.GetMostRecentItems(message.Count);
+	private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) {
+		return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
 	}
 
 	private void HandleReply(ReplyMessage message) {
diff --git a/Controller/Phantom.Controller.Services/Rpc/WebRegistrationHandler.cs b/Controller/Phantom.Controller.Services/Rpc/WebRegistrationHandler.cs
index 3c3319c..7217592 100644
--- a/Controller/Phantom.Controller.Services/Rpc/WebRegistrationHandler.cs
+++ b/Controller/Phantom.Controller.Services/Rpc/WebRegistrationHandler.cs
@@ -7,6 +7,7 @@ using Phantom.Controller.Services.Agents;
 using Phantom.Controller.Services.Events;
 using Phantom.Controller.Services.Instances;
 using Phantom.Controller.Services.Users;
+using Phantom.Controller.Services.Users.Sessions;
 using Phantom.Utils.Actor;
 using Phantom.Utils.Logging;
 using Phantom.Utils.Rpc.Runtime;
diff --git a/Controller/Phantom.Controller.Services/Users/AuditLogManager.cs b/Controller/Phantom.Controller.Services/Users/AuditLogManager.cs
index 1511802..bf9d589 100644
--- a/Controller/Phantom.Controller.Services/Users/AuditLogManager.cs
+++ b/Controller/Phantom.Controller.Services/Users/AuditLogManager.cs
@@ -1,7 +1,10 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.AuditLog;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Controller.Database;
 using Phantom.Controller.Database.Repositories;
+using Phantom.Controller.Services.Users.Sessions;
 
 namespace Phantom.Controller.Services.Users; 
 
@@ -12,7 +15,11 @@ sealed class AuditLogManager {
 		this.dbProvider = dbProvider;
 	}
 
-	public async Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count) {
+	public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) {
+		if (!loggedInUser.CheckPermission(Permission.ViewAudit)) {
+			return UserActionFailure.NotAuthorized;
+		}
+		
 		await using var db = dbProvider.Lazy();
 		return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None);
 	}
diff --git a/Controller/Phantom.Controller.Services/Users/PermissionManager.cs b/Controller/Phantom.Controller.Services/Users/PermissionManager.cs
index 5427c3e..848608e 100644
--- a/Controller/Phantom.Controller.Services/Users/PermissionManager.cs
+++ b/Controller/Phantom.Controller.Services/Users/PermissionManager.cs
@@ -36,34 +36,6 @@ sealed class PermissionManager {
 		}
 	}
 
-	public async Task<PermissionSet> FetchPermissionsForAllUsers(Guid userId) {
-		await using var ctx = dbProvider.Eager();
-		
-		var userPermissions = ctx.UserPermissions
-		                         .Where(up => up.UserGuid == userId)
-		                         .Select(static up => up.PermissionId);
-		
-		var rolePermissions = ctx.UserRoles
-		                         .Where(ur => ur.UserGuid == userId)
-		                         .Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
-		
-		return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
-	}
-	
-	public async Task<PermissionSet> FetchPermissionsForUserId(Guid userId) {
-		await using var ctx = dbProvider.Eager();
-		
-		var userPermissions = ctx.UserPermissions
-		                         .Where(up => up.UserGuid == userId)
-		                         .Select(static up => up.PermissionId);
-		
-		var rolePermissions = ctx.UserRoles
-		                         .Where(ur => ur.UserGuid == userId)
-		                         .Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
-		
-		return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
-	}
-
 	public static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
 		return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
 	}
diff --git a/Controller/Phantom.Controller.Services/Users/Sessions/AuthenticatedUserCache.cs b/Controller/Phantom.Controller.Services/Users/Sessions/AuthenticatedUserCache.cs
new file mode 100644
index 0000000..7a77c4c
--- /dev/null
+++ b/Controller/Phantom.Controller.Services/Users/Sessions/AuthenticatedUserCache.cs
@@ -0,0 +1,26 @@
+using System.Collections.Concurrent;
+using Phantom.Common.Data.Web.Users;
+using Phantom.Controller.Database;
+using Phantom.Controller.Database.Entities;
+using Phantom.Controller.Database.Repositories;
+
+namespace Phantom.Controller.Services.Users.Sessions;
+
+sealed class AuthenticatedUserCache {
+	private readonly ConcurrentDictionary<Guid, AuthenticatedUserInfo> authenticatedUsersByGuid = new ();
+
+	public bool TryGet(Guid userGuid, out AuthenticatedUserInfo? userInfo) {
+		return authenticatedUsersByGuid.TryGetValue(userGuid, out userInfo);
+	}
+
+	public async Task<AuthenticatedUserInfo?> Update(UserEntity user, ILazyDbContext db) {
+		var userGuid = user.UserGuid;
+		var userPermissions = await new PermissionRepository(db).GetAllUserPermissions(user);
+		var userInfo = new AuthenticatedUserInfo(userGuid, user.Name, userPermissions);
+		return authenticatedUsersByGuid[userGuid] = userInfo;
+	}
+	
+	public void Remove(Guid userGuid) {
+		authenticatedUsersByGuid.Remove(userGuid, out _);
+	}
+}
diff --git a/Controller/Phantom.Controller.Services/Users/Sessions/LoggedInUser.cs b/Controller/Phantom.Controller.Services/Users/Sessions/LoggedInUser.cs
new file mode 100644
index 0000000..583c03b
--- /dev/null
+++ b/Controller/Phantom.Controller.Services/Users/Sessions/LoggedInUser.cs
@@ -0,0 +1,11 @@
+using Phantom.Common.Data.Web.Users;
+
+namespace Phantom.Controller.Services.Users.Sessions;
+
+readonly record struct LoggedInUser(AuthenticatedUserInfo? AuthenticatedUserInfo) {
+	public Guid? Guid => AuthenticatedUserInfo?.Guid;
+	
+	public bool CheckPermission(Permission permission) {
+		return AuthenticatedUserInfo != null && AuthenticatedUserInfo.Permissions.Check(permission);
+	}
+}
diff --git a/Controller/Phantom.Controller.Services/Users/Sessions/UserLoginManager.cs b/Controller/Phantom.Controller.Services/Users/Sessions/UserLoginManager.cs
new file mode 100644
index 0000000..aa3157a
--- /dev/null
+++ b/Controller/Phantom.Controller.Services/Users/Sessions/UserLoginManager.cs
@@ -0,0 +1,139 @@
+using System.Collections.Immutable;
+using System.Security.Cryptography;
+using Phantom.Common.Data.Web.Users;
+using Phantom.Controller.Database;
+using Phantom.Controller.Database.Repositories;
+
+namespace Phantom.Controller.Services.Users.Sessions;
+
+sealed class UserLoginManager {
+	private const int SessionIdBytes = 20;
+
+	private readonly AuthenticatedUserCache authenticatedUserCache;
+	private readonly UserManager userManager;
+	private readonly IDbContextProvider dbProvider;
+	
+	private readonly UserSessionBucket[] sessionBuckets = new UserSessionBucket[256];
+
+	public UserLoginManager(AuthenticatedUserCache authenticatedUserCache, UserManager userManager, IDbContextProvider dbProvider) {
+		this.authenticatedUserCache = authenticatedUserCache;
+		this.userManager = userManager;
+		this.dbProvider = dbProvider;
+
+		for (int i = 0; i < sessionBuckets.GetLength(0); i++) {
+			sessionBuckets[i] = new UserSessionBucket();
+		}
+	}
+
+	private UserSessionBucket GetSessionBucket(ImmutableArray<byte> token) {
+		return sessionBuckets[token[0]];
+	}
+
+	public async Task<LogInSuccess?> LogIn(string username, string password) {
+		Guid userGuid;
+		AuthenticatedUserInfo? authenticatedUserInfo;
+		
+		await using (var db = dbProvider.Lazy()) {
+			var userRepository = new UserRepository(db);
+
+			var user = await userRepository.GetByName(username);
+			if (user == null || !UserPasswords.Verify(password, user.PasswordHash)) {
+				return null;
+			}
+
+			authenticatedUserInfo = await authenticatedUserCache.Update(user, db);
+			if (authenticatedUserInfo == null) {
+				return null;
+			}
+
+			userGuid = user.UserGuid;
+
+			var auditLogWriter = new AuditLogRepository(db).Writer(userGuid);
+			auditLogWriter.UserLoggedIn(user);
+
+			await db.Ctx.SaveChangesAsync();
+		}
+
+		var authToken = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
+		GetSessionBucket(authToken).Add(userGuid, authToken);
+		
+		return new LogInSuccess(authenticatedUserInfo, authToken);
+	}
+
+	public async Task LogOut(Guid userGuid, ImmutableArray<byte> authToken) {
+		if (!GetSessionBucket(authToken).Remove(userGuid, authToken)) {
+			return;
+		}
+
+		await using var db = dbProvider.Lazy();
+
+		var auditLogWriter = new AuditLogRepository(db).Writer(userGuid);
+		auditLogWriter.UserLoggedOut(userGuid);
+
+		await db.Ctx.SaveChangesAsync();
+	}
+
+	public LoggedInUser GetLoggedInUser(ImmutableArray<byte> authToken) {
+		var userGuid = GetSessionBucket(authToken).FindUserGuid(authToken);
+		return userGuid != null && authenticatedUserCache.TryGet(userGuid.Value, out var userInfo) ? new LoggedInUser(userInfo) : default;
+	}
+	
+	public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> authToken) {
+		return authenticatedUserCache.TryGet(userGuid, out var userInfo) && GetSessionBucket(authToken).Contains(userGuid, authToken) ? userInfo : null;
+	}
+
+	private sealed class UserSessionBucket {
+		private ImmutableList<UserSession> sessions = ImmutableList<UserSession>.Empty;
+
+		public void Add(Guid userGuid, ImmutableArray<byte> authToken) {
+			lock (this) {
+				var session = new UserSession(userGuid, authToken);
+				if (!sessions.Contains(session)) {
+					sessions = sessions.Add(session);
+				}
+			}
+		}
+
+		public bool Contains(Guid userGuid, ImmutableArray<byte> authToken) {
+			lock (this) {
+				return sessions.Contains(new UserSession(userGuid, authToken));
+			}
+		}
+
+		public Guid? FindUserGuid(ImmutableArray<byte> authToken) {
+			lock (this) {
+				return sessions.Find(session => session.AuthTokenEquals(authToken))?.UserGuid;
+			}
+		}
+
+		public bool Remove(Guid userGuid, ImmutableArray<byte> authToken) {
+			lock (this) {
+				int index = sessions.IndexOf(new UserSession(userGuid, authToken));
+				if (index == -1) {
+					return false;
+				}
+
+				sessions = sessions.RemoveAt(index);
+				return true;
+			}
+		}
+	}
+
+	private sealed record UserSession(Guid UserGuid, ImmutableArray<byte> AuthToken) {
+		public bool AuthTokenEquals(ImmutableArray<byte> other) {
+			return CryptographicOperations.FixedTimeEquals(AuthToken.AsSpan(), other.AsSpan());
+		}
+
+		public bool Equals(UserSession? other) {
+			if (ReferenceEquals(null, other)) {
+				return false;
+			}
+			
+			return UserGuid.Equals(other.UserGuid) && AuthTokenEquals(other.AuthToken);
+		}
+
+		public override int GetHashCode() {
+			throw new NotImplementedException();
+		}
+	}
+}
diff --git a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs
deleted file mode 100644
index 6584908..0000000
--- a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System.Collections.Concurrent;
-using System.Collections.Immutable;
-using System.Runtime.CompilerServices;
-using System.Security.Cryptography;
-using Phantom.Common.Data.Web.Users;
-using Phantom.Controller.Database;
-using Phantom.Controller.Database.Repositories;
-
-namespace Phantom.Controller.Services.Users; 
-
-sealed class UserLoginManager {
-	private const int SessionIdBytes = 20;
-	private readonly ConcurrentDictionary<Guid, UserSession> sessionsByUserGuid = new ();
-	
-	private readonly UserManager userManager;
-	private readonly PermissionManager permissionManager;
-	private readonly IDbContextProvider dbProvider;
-	
-	public UserLoginManager(UserManager userManager, PermissionManager permissionManager, IDbContextProvider dbProvider) {
-		this.userManager = userManager;
-		this.permissionManager = permissionManager;
-		this.dbProvider = dbProvider;
-	}
-
-	public async Task<LogInSuccess?> LogIn(string username, string password) {
-		var user = await userManager.GetAuthenticated(username, password);
-		if (user == null) {
-			return null;
-		}
-
-		var permissions = await permissionManager.FetchPermissionsForUserId(user.UserGuid);
-		var userInfo = new AuthenticatedUserInfo(user.UserGuid, user.Name, permissions);
-		var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
-		
-		sessionsByUserGuid.AddOrUpdate(user.UserGuid, UserSession.Create, UserSession.Add, new NewUserSession(userInfo, token));
-
-		await using (var db = dbProvider.Lazy()) {
-			var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid);
-			auditLogWriter.UserLoggedIn(user);
-			
-			await db.Ctx.SaveChangesAsync();
-		}
-
-		return new LogInSuccess(userInfo, token);
-	}
-
-	public async Task LogOut(Guid userGuid, ImmutableArray<byte> token) {
-		while (true) {
-			if (!sessionsByUserGuid.TryGetValue(userGuid, out var oldSession)) {
-				return;
-			}
-
-			if (sessionsByUserGuid.TryUpdate(userGuid, oldSession.RemoveToken(token), oldSession)) {
-				break;
-			}
-		}
-
-		await using var db = dbProvider.Lazy();
-		
-		var auditLogWriter = new AuditLogRepository(db).Writer(userGuid);
-		auditLogWriter.UserLoggedOut(userGuid);
-			
-		await db.Ctx.SaveChangesAsync();
-	}
-
-	public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> token) {
-		return sessionsByUserGuid.TryGetValue(userGuid, out var session) && session.Tokens.Contains(token, TokenEqualityComparer.Instance) ? session.UserInfo : null;
-	}
-
-	private readonly record struct NewUserSession(AuthenticatedUserInfo UserInfo, ImmutableArray<byte> Token);
-	
-	private sealed record UserSession(AuthenticatedUserInfo UserInfo, ImmutableList<ImmutableArray<byte>> Tokens) {
-		public static UserSession Create(Guid userGuid, NewUserSession newSession) {
-			return new UserSession(newSession.UserInfo, ImmutableList.Create(newSession.Token));
-		}
-		
-		public static UserSession Add(Guid userGuid, UserSession oldSession, NewUserSession newSession) {
-			return new UserSession(newSession.UserInfo, oldSession.Tokens.Add(newSession.Token));
-		}
-
-		public UserSession RemoveToken(ImmutableArray<byte> token) {
-			return this with { Tokens = Tokens.Remove(token, TokenEqualityComparer.Instance) };
-		}
-
-		public bool Equals(UserSession? other) {
-			return ReferenceEquals(this, other);
-		}
-
-		public override int GetHashCode() {
-			return RuntimeHelpers.GetHashCode(this);
-		}
-	}
-
-	private sealed class TokenEqualityComparer : IEqualityComparer<ImmutableArray<byte>> {
-		public static TokenEqualityComparer Instance { get; } = new ();
-		
-		private TokenEqualityComparer() {}
-
-		public bool Equals(ImmutableArray<byte> x, ImmutableArray<byte> y) {
-			return x.SequenceEqual(y);
-		}
-
-		public int GetHashCode(ImmutableArray<byte> obj) {
-			throw new NotImplementedException();
-		}
-	}
-}
diff --git a/Controller/Phantom.Controller.Services/Users/UserManager.cs b/Controller/Phantom.Controller.Services/Users/UserManager.cs
index 571c533..5045f59 100644
--- a/Controller/Phantom.Controller.Services/Users/UserManager.cs
+++ b/Controller/Phantom.Controller.Services/Users/UserManager.cs
@@ -1,8 +1,10 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Controller.Database;
 using Phantom.Controller.Database.Entities;
 using Phantom.Controller.Database.Repositories;
+using Phantom.Controller.Services.Users.Sessions;
 using Phantom.Utils.Logging;
 using Serilog;
 
@@ -11,9 +13,11 @@ namespace Phantom.Controller.Services.Users;
 sealed class UserManager {
 	private static readonly ILogger Logger = PhantomLogger.Create<UserManager>();
 
+	private readonly AuthenticatedUserCache authenticatedUserCache;
 	private readonly IDbContextProvider dbProvider;
 
-	public UserManager(IDbContextProvider dbProvider) {
+	public UserManager(AuthenticatedUserCache authenticatedUserCache, IDbContextProvider dbProvider) {
+		this.authenticatedUserCache = authenticatedUserCache;
 		this.dbProvider = dbProvider;
 	}
 
@@ -85,10 +89,14 @@ sealed class UserManager {
 		}
 	}
 
-	public async Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password) {
+	public async Task<Result<CreateUserResult, UserActionFailure>> Create(LoggedInUser loggedInUser, string username, string password) {
+		if (!loggedInUser.CheckPermission(Permission.EditUsers)) {
+			return UserActionFailure.NotAuthorized;
+		}
+		
 		await using var db = dbProvider.Lazy();
 		var userRepository = new UserRepository(db);
-		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
+		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
 
 		try {
 			var result = await userRepository.CreateUser(username, password);
@@ -109,7 +117,11 @@ sealed class UserManager {
 		}
 	}
 	
-	public async Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid) {
+	public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(LoggedInUser loggedInUser, Guid userGuid) {
+		if (!loggedInUser.CheckPermission(Permission.EditUsers)) {
+			return UserActionFailure.NotAuthorized;
+		}
+		
 		await using var db = dbProvider.Lazy();
 		var userRepository = new UserRepository(db);
 
@@ -118,11 +130,16 @@ sealed class UserManager {
 			return DeleteUserResult.NotFound;
 		}
 
-		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
+		authenticatedUserCache.Remove(userGuid);
+		
+		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
 		try {
 			userRepository.DeleteUser(user);
 			auditLogWriter.UserDeleted(user);
 			await db.Ctx.SaveChangesAsync();
+			
+			// In case the user logged in during deletion.
+			authenticatedUserCache.Remove(userGuid);
 
 			Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
 			return DeleteUserResult.Deleted;
diff --git a/Controller/Phantom.Controller.Services/Users/UserRoleManager.cs b/Controller/Phantom.Controller.Services/Users/UserRoleManager.cs
index 56e0b13..b71e870 100644
--- a/Controller/Phantom.Controller.Services/Users/UserRoleManager.cs
+++ b/Controller/Phantom.Controller.Services/Users/UserRoleManager.cs
@@ -1,7 +1,9 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Controller.Database;
 using Phantom.Controller.Database.Repositories;
+using Phantom.Controller.Services.Users.Sessions;
 using Phantom.Utils.Logging;
 using Serilog;
 
@@ -9,10 +11,12 @@ namespace Phantom.Controller.Services.Users;
 
 sealed class UserRoleManager {
 	private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
-	
+
+	private readonly AuthenticatedUserCache authenticatedUserCache;
 	private readonly IDbContextProvider dbProvider;
 	
-	public UserRoleManager(IDbContextProvider dbProvider) {
+	public UserRoleManager(AuthenticatedUserCache authenticatedUserCache, IDbContextProvider dbProvider) {
+		this.authenticatedUserCache = authenticatedUserCache;
 		this.dbProvider = dbProvider;
 	}
 
@@ -21,7 +25,11 @@ sealed class UserRoleManager {
 		return await new UserRoleRepository(db).GetRoleGuidsByUserGuid(userGuids);
 	}
 
-	public async Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) {
+	public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(LoggedInUser loggedInUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) {
+		if (!loggedInUser.CheckPermission(Permission.EditUsers)) {
+			return UserActionFailure.NotAuthorized;
+		}
+		
 		await using var db = dbProvider.Lazy();
 		var userRepository = new UserRepository(db);
 		
@@ -32,7 +40,7 @@ sealed class UserRoleManager {
 
 		var roleRepository = new RoleRepository(db);
 		var userRoleRepository = new UserRoleRepository(db);
-		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
+		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
 		
 		var rolesByGuid = await roleRepository.GetByGuids(addToRoleGuids.Union(removeFromRoleGuids));
 		
@@ -62,6 +70,8 @@ sealed class UserRoleManager {
 			auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames);
 			await db.Ctx.SaveChangesAsync();
 			
+			await authenticatedUserCache.Update(user, db);
+			
 			Logger.Information("Changed roles for user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
 			return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable());
 		} catch (Exception e) {
diff --git a/Utils/Phantom.Utils.Actor/Tasks/TaskExtensions.cs b/Utils/Phantom.Utils.Actor/Tasks/TaskExtensions.cs
index 4aa01b9..14a5895 100644
--- a/Utils/Phantom.Utils.Actor/Tasks/TaskExtensions.cs
+++ b/Utils/Phantom.Utils.Actor/Tasks/TaskExtensions.cs
@@ -17,6 +17,10 @@ public static class TaskExtensions {
 		return task.ContinueOnActor(result => mapper(result, arg));
 	}
 	
+	public static Task<TResult> ContinueOnActor<TSource, TArg1, TArg2, TResult>(this Task<TSource> task, Func<TSource, TArg1, TArg2, TResult> mapper, TArg1 arg1, TArg2 arg2) {
+		return task.ContinueOnActor(result => mapper(result, arg1, arg2));
+	}
+	
 	private static Task<TResult> MapResult<TSource, TResult>(Task<TSource> task, Func<TSource, TResult> mapper, TaskCompletionSource<TResult> completionSource) {
 		if (task.IsFaulted) {
 			completionSource.SetException(task.Exception.InnerExceptions);
diff --git a/Utils/Phantom.Utils/Result/Err.cs b/Utils/Phantom.Utils/Result/Err.cs
new file mode 100644
index 0000000..839bec8
--- /dev/null
+++ b/Utils/Phantom.Utils/Result/Err.cs
@@ -0,0 +1,3 @@
+namespace Phantom.Utils.Result;
+
+public sealed record Err<T>(T Error) : Result;
diff --git a/Utils/Phantom.Utils/Result/Ok.cs b/Utils/Phantom.Utils/Result/Ok.cs
new file mode 100644
index 0000000..8f47b2f
--- /dev/null
+++ b/Utils/Phantom.Utils/Result/Ok.cs
@@ -0,0 +1,3 @@
+namespace Phantom.Utils.Result;
+
+public sealed record Ok<T>(T Value) : Result;
diff --git a/Utils/Phantom.Utils/Result/Result.cs b/Utils/Phantom.Utils/Result/Result.cs
new file mode 100644
index 0000000..50f2fb4
--- /dev/null
+++ b/Utils/Phantom.Utils/Result/Result.cs
@@ -0,0 +1,5 @@
+namespace Phantom.Utils.Result;
+
+public abstract record Result {
+	private protected Result() {}
+}
diff --git a/Web/Phantom.Web.Components/PhantomComponent.cs b/Web/Phantom.Web.Components/PhantomComponent.cs
index d593732..ba42188 100644
--- a/Web/Phantom.Web.Components/PhantomComponent.cs
+++ b/Web/Phantom.Web.Components/PhantomComponent.cs
@@ -17,11 +17,11 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
 
 	protected CancellationToken CancellationToken => cancellationTokenSource.Token;
 
-	protected async Task<Guid?> GetUserGuid() {
+	protected async Task<AuthenticatedUser?> GetAuthenticatedUser() {
 		var authenticationState = await AuthenticationStateTask;
-		return authenticationState.TryGetGuid();
+		return authenticationState.GetAuthenticatedUser();
 	}
-
+	
 	protected async Task<bool> CheckPermission(Permission permission) {
 		var authenticationState = await AuthenticationStateTask;
 		return authenticationState.CheckPermission(permission);
diff --git a/Web/Phantom.Web.Services/Authentication/AuthenticatedUser.cs b/Web/Phantom.Web.Services/Authentication/AuthenticatedUser.cs
new file mode 100644
index 0000000..8f024b6
--- /dev/null
+++ b/Web/Phantom.Web.Services/Authentication/AuthenticatedUser.cs
@@ -0,0 +1,10 @@
+using System.Collections.Immutable;
+using Phantom.Common.Data.Web.Users;
+
+namespace Phantom.Web.Services.Authentication;
+
+public sealed record AuthenticatedUser(AuthenticatedUserInfo Info, ImmutableArray<byte> Token) {
+	public bool CheckPermission(Permission permission) {
+		return Info.Permissions.Check(permission);
+	}
+}
diff --git a/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs b/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs
index 18e7907..4a9603e 100644
--- a/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs
+++ b/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs
@@ -5,23 +5,27 @@ using Phantom.Common.Data.Web.Users;
 namespace Phantom.Web.Services.Authentication;
 
 public static class AuthenticationStateExtensions {
-	public static Guid? TryGetGuid(this AuthenticationState authenticationState) {
-		return authenticationState.User is CustomClaimsPrincipal customUser ? customUser.UserInfo.Guid : null;
+	public static AuthenticatedUser? GetAuthenticatedUser(this AuthenticationState authenticationState) {
+		return authenticationState.User.GetAuthenticatedUser();
 	}
 
-	public static PermissionSet GetPermissions(this ClaimsPrincipal user) {
-		return user is CustomClaimsPrincipal customUser ? customUser.UserInfo.Permissions : PermissionSet.None;
-	}
-
-	public static bool CheckPermission(this ClaimsPrincipal user, Permission permission) {
-		return user.GetPermissions().Check(permission);
+	public static AuthenticatedUser? GetAuthenticatedUser(this ClaimsPrincipal claimsPrincipal) {
+		return claimsPrincipal is CustomClaimsPrincipal principal ? principal.User : null;
 	}
 
 	public static PermissionSet GetPermissions(this AuthenticationState authenticationState) {
 		return authenticationState.User.GetPermissions();
 	}
+	
+	public static PermissionSet GetPermissions(this ClaimsPrincipal claimsPrincipal) {
+		return claimsPrincipal.GetAuthenticatedUser() is {} user ? user.Info.Permissions : PermissionSet.None;
+	}
 
 	public static bool CheckPermission(this AuthenticationState authenticationState, Permission permission) {
 		return authenticationState.User.CheckPermission(permission);
 	}
+	
+	public static bool CheckPermission(this ClaimsPrincipal claimsPrincipal, Permission permission) {
+		return claimsPrincipal.GetPermissions().Check(permission);
+	}
 }
diff --git a/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs b/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs
index c2ae175..f4fdfb0 100644
--- a/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs
+++ b/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs
@@ -22,9 +22,10 @@ public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStat
 		if (!isLoaded) {
 			var stored = await sessionBrowserStorage.Get();
 			if (stored != null) {
-				var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, stored.Token), TimeSpan.FromSeconds(30));
+				var authToken = stored.Token;
+				var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, authToken), TimeSpan.FromSeconds(30));
 				if (session.Value is {} userInfo) {
-					SetLoadedSession(userInfo);
+					SetLoadedSession(new AuthenticatedUser(userInfo, authToken));
 				}
 			}
 		}
@@ -32,9 +33,9 @@ public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStat
 		return await base.GetAuthenticationStateAsync();
 	}
 
-	internal void SetLoadedSession(AuthenticatedUserInfo user) {
+	internal void SetLoadedSession(AuthenticatedUser authenticatedUser) {
 		isLoaded = true;
-		SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(user))));
+		SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(authenticatedUser))));
 	}
 
 	internal void SetUnloadedSession() {
diff --git a/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs b/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs
index 3a0188d..4946287 100644
--- a/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs
+++ b/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs
@@ -4,10 +4,10 @@ using Phantom.Common.Data.Web.Users;
 namespace Phantom.Web.Services.Authentication;
 
 sealed class CustomClaimsPrincipal : ClaimsPrincipal {
-	internal AuthenticatedUserInfo UserInfo { get; }
+	internal AuthenticatedUser User { get; }
 
-	internal CustomClaimsPrincipal(AuthenticatedUserInfo userInfo) : base(GetIdentity(userInfo)) {
-		UserInfo = userInfo;
+	internal CustomClaimsPrincipal(AuthenticatedUser user) : base(GetIdentity(user.Info)) {
+		User = user;
 	}
 
 	private static ClaimsIdentity GetIdentity(AuthenticatedUserInfo userInfo) {
diff --git a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
index 6a1c914..a90aef0 100644
--- a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
+++ b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
@@ -37,9 +37,10 @@ public sealed class UserLoginManager {
 		Logger.Information("Successfully logged in {Username}.", username);
 
 		var userInfo = success.UserInfo;
-
-		await sessionBrowserStorage.Store(userInfo.Guid, success.Token);
-		authenticationStateProvider.SetLoadedSession(userInfo);
+		var authToken = success.AuthToken;
+		
+		await sessionBrowserStorage.Store(userInfo.Guid, authToken);
+		authenticationStateProvider.SetLoadedSession(new AuthenticatedUser(userInfo, authToken));
 		await navigation.NavigateTo(returnUrl ?? string.Empty);
 		
 		return true;
diff --git a/Web/Phantom.Web.Services/Events/EventLogManager.cs b/Web/Phantom.Web.Services/Events/EventLogManager.cs
index 588b2dc..0bb5f55 100644
--- a/Web/Phantom.Web.Services/Events/EventLogManager.cs
+++ b/Web/Phantom.Web.Services/Events/EventLogManager.cs
@@ -1,6 +1,9 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.EventLog;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Common.Messages.Web.ToController;
+using Phantom.Web.Services.Authentication;
 using Phantom.Web.Services.Rpc;
 
 namespace Phantom.Web.Services.Events; 
@@ -12,8 +15,13 @@ public sealed class EventLogManager {
 		this.controllerConnection = controllerConnection;
 	}
 
-	public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
-		var message = new GetEventLogMessage(count);
-		return controllerConnection.Send<GetEventLogMessage, ImmutableArray<EventLogItem>>(message, cancellationToken);
+	public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ViewEvents)) {
+			var message = new GetEventLogMessage(authenticatedUser.Token, count);
+			return await controllerConnection.Send<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(message, cancellationToken);
+		}
+		else {
+			return UserActionFailure.NotAuthorized;
+		}
 	}
 }
diff --git a/Web/Phantom.Web.Services/Instances/InstanceManager.cs b/Web/Phantom.Web.Services/Instances/InstanceManager.cs
index 2794299..98f1633 100644
--- a/Web/Phantom.Web.Services/Instances/InstanceManager.cs
+++ b/Web/Phantom.Web.Services/Instances/InstanceManager.cs
@@ -4,9 +4,11 @@ using Phantom.Common.Data.Instance;
 using Phantom.Common.Data.Minecraft;
 using Phantom.Common.Data.Replies;
 using Phantom.Common.Data.Web.Instance;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Common.Messages.Web.ToController;
 using Phantom.Utils.Events;
 using Phantom.Utils.Logging;
+using Phantom.Web.Services.Authentication;
 using Phantom.Web.Services.Rpc;
 
 namespace Phantom.Web.Services.Instances;
@@ -35,23 +37,43 @@ public sealed class InstanceManager {
 		return instances.Value.GetValueOrDefault(instanceGuid);
 	}
 
-	public Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(Guid loggedInUserGuid, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
-		var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, instanceGuid, configuration);
-		return controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(message, cancellationToken);
+	public async Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(AuthenticatedUser? authenticatedUser, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.CreateInstances)) {
+			var message = new CreateOrUpdateInstanceMessage(authenticatedUser.Token, instanceGuid, configuration);
+			return await controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(message, cancellationToken);
+		}
+		else {
+			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
+		}
 	}
 
-	public Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) {
-		var message = new LaunchInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid);
-		return controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(message, cancellationToken);
+	public async Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) {
+			var message = new LaunchInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid);
+			return await controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(message, cancellationToken);
+		}
+		else {
+			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
+		}
 	}
 
-	public Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
-		var message = new StopInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, stopStrategy);
-		return controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(message, cancellationToken);
+	public async Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) {
+			var message = new StopInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid, stopStrategy);
+			return await controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(message, cancellationToken);
+		}
+		else {
+			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
+		}
 	}
 
-	public Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) {
-		var message = new SendCommandToInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, command);
-		return controllerConnection.Send<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(message, cancellationToken);
+	public async Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> SendCommandToInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) {
+			var message = new SendCommandToInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid, command);
+			return await controllerConnection.Send<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(message, cancellationToken);
+		}
+		else {
+			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
+		}
 	}
 }
diff --git a/Web/Phantom.Web.Services/Users/AuditLogManager.cs b/Web/Phantom.Web.Services/Users/AuditLogManager.cs
index 9f521e7..ff20c4c 100644
--- a/Web/Phantom.Web.Services/Users/AuditLogManager.cs
+++ b/Web/Phantom.Web.Services/Users/AuditLogManager.cs
@@ -1,6 +1,9 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.AuditLog;
+using Phantom.Common.Data.Web.Users;
 using Phantom.Common.Messages.Web.ToController;
+using Phantom.Web.Services.Authentication;
 using Phantom.Web.Services.Rpc;
 
 namespace Phantom.Web.Services.Users; 
@@ -12,8 +15,13 @@ public sealed class AuditLogManager {
 		this.controllerConnection = controllerConnection;
 	}
 
-	public Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
-		var message = new GetAuditLogMessage(count);
-		return controllerConnection.Send<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(message, cancellationToken);
+	public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ViewAudit)) {
+			var message = new GetAuditLogMessage(authenticatedUser.Token, count);
+			return await controllerConnection.Send<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(message, cancellationToken);
+		}
+		else {
+			return UserActionFailure.NotAuthorized;
+		}
 	}
 }
diff --git a/Web/Phantom.Web.Services/Users/UserManager.cs b/Web/Phantom.Web.Services/Users/UserManager.cs
index eb0502a..30edf57 100644
--- a/Web/Phantom.Web.Services/Users/UserManager.cs
+++ b/Web/Phantom.Web.Services/Users/UserManager.cs
@@ -1,6 +1,8 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Common.Messages.Web.ToController;
+using Phantom.Web.Services.Authentication;
 using Phantom.Web.Services.Rpc;
 
 namespace Phantom.Web.Services.Users;
@@ -16,11 +18,21 @@ public sealed class UserManager {
 		return controllerConnection.Send<GetUsersMessage, ImmutableArray<UserInfo>>(new GetUsersMessage(), cancellationToken);
 	}
 
-	public Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password, CancellationToken cancellationToken) {
-		return controllerConnection.Send<CreateUserMessage, CreateUserResult>(new CreateUserMessage(loggedInUserGuid, username, password), cancellationToken);
+	public async Task<Result<CreateUserResult, UserActionFailure>> Create(AuthenticatedUser? authenticatedUser, string username, string password, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) {
+			return await controllerConnection.Send<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(new CreateUserMessage(authenticatedUser.Token, username, password), cancellationToken);
+		}
+		else {
+			return UserActionFailure.NotAuthorized;
+		}
 	}
 	
-	public Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid, CancellationToken cancellationToken) {
-		return controllerConnection.Send<DeleteUserMessage, DeleteUserResult>(new DeleteUserMessage(loggedInUserGuid, userGuid), cancellationToken);
+	public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(AuthenticatedUser? authenticatedUser, Guid userGuid, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) {
+			return await controllerConnection.Send<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(new DeleteUserMessage(authenticatedUser.Token, userGuid), cancellationToken);
+		}
+		else {
+			return UserActionFailure.NotAuthorized;
+		}
 	}
 }
diff --git a/Web/Phantom.Web.Services/Users/UserRoleManager.cs b/Web/Phantom.Web.Services/Users/UserRoleManager.cs
index 06a5f7d..2fd20b1 100644
--- a/Web/Phantom.Web.Services/Users/UserRoleManager.cs
+++ b/Web/Phantom.Web.Services/Users/UserRoleManager.cs
@@ -1,6 +1,8 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Common.Messages.Web.ToController;
+using Phantom.Web.Services.Authentication;
 using Phantom.Web.Services.Rpc;
 
 namespace Phantom.Web.Services.Users;
@@ -20,7 +22,12 @@ public sealed class UserRoleManager {
 		return (await GetUserRoles(ImmutableHashSet.Create(userGuid), cancellationToken)).GetValueOrDefault(userGuid, ImmutableArray<Guid>.Empty);
 	}
 
-	public Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken cancellationToken) {
-		return controllerConnection.Send<ChangeUserRolesMessage, ChangeUserRolesResult>(new ChangeUserRolesMessage(loggedInUserGuid, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken);
+	public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(AuthenticatedUser? authenticatedUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken cancellationToken) {
+		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) {
+			return await controllerConnection.Send<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(new ChangeUserRolesMessage(authenticatedUser.Token, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken);
+		}
+		else {
+			return UserActionFailure.NotAuthorized;
+		}
 	}
 }
diff --git a/Web/Phantom.Web/Pages/Audit.razor b/Web/Phantom.Web/Pages/Audit.razor
index 478dcff..f1b471b 100644
--- a/Web/Phantom.Web/Pages/Audit.razor
+++ b/Web/Phantom.Web/Pages/Audit.razor
@@ -5,13 +5,18 @@
 @using Phantom.Common.Data.Web.Users
 @using Phantom.Web.Services.Users
 @using Phantom.Web.Services.Instances
-@inherits Phantom.Web.Components.PhantomComponent
+@inherits PhantomComponent
 @inject AuditLogManager AuditLogManager
 @inject InstanceManager InstanceManager
 @inject UserManager UserManager
 
 <h1>Audit Log</h1>
 
+@if (loadError is {} error) {
+  <p>@error</p>
+  return;
+}
+
 <Table TItem="AuditLogItem" Items="logItems">
   <HeaderRow>
     <Column Class="text-end" MinWidth="200px">Time</Column>
@@ -46,21 +51,25 @@
 
 @code {
 
-  private CancellationTokenSource? initializationCancellationTokenSource;
   private ImmutableArray<AuditLogItem>? logItems;
+  private string? loadError;
+  
   private ImmutableDictionary<Guid, string>? userNamesByGuid;
   private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
 
   protected override async Task OnInitializedAsync() {
-    initializationCancellationTokenSource = new CancellationTokenSource();
-    var cancellationToken = initializationCancellationTokenSource.Token;
-
-    try {
-      logItems = await AuditLogManager.GetMostRecentItems(50, cancellationToken);
-      userNamesByGuid = (await UserManager.GetAll(cancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
+    var result = await AuditLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken);
+    if (result) {
+      logItems = result.Value;
+      userNamesByGuid = (await UserManager.GetAll(CancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
       instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName);
-    } finally {
-      initializationCancellationTokenSource.Dispose();
+    }
+    else {
+      logItems = ImmutableArray<AuditLogItem>.Empty;
+      loadError = result.Error switch {
+        UserActionFailure.NotAuthorized => "You do not have permission to view the audit log.",
+        _                               => "Unknown error."
+      };
     }
   }
 
@@ -72,10 +81,4 @@
     };
   }
 
-  protected override void OnDisposed() {
-    try {
-      initializationCancellationTokenSource?.Cancel();
-    } catch (ObjectDisposedException) {}
-  }
-
 }
diff --git a/Web/Phantom.Web/Pages/Events.razor b/Web/Phantom.Web/Pages/Events.razor
index 11f91a8..7ab5f21 100644
--- a/Web/Phantom.Web/Pages/Events.razor
+++ b/Web/Phantom.Web/Pages/Events.razor
@@ -13,6 +13,11 @@
 
 <h1>Event Log</h1>
 
+@if (loadError is {} error) {
+  <p>@error</p>
+  return;
+}
+
 <Table TItem="EventLogItem" Items="logItems">
   <HeaderRow>
     <Column Class="text-end" MinWidth="200px">Time</Column>
@@ -50,21 +55,25 @@
 
 @code {
 
-  private CancellationTokenSource? initializationCancellationTokenSource;
   private ImmutableArray<EventLogItem>? logItems;
+  private string? loadError;
+  
   private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
   private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
 
   protected override async Task OnInitializedAsync() {
-    initializationCancellationTokenSource = new CancellationTokenSource();
-    var cancellationToken = initializationCancellationTokenSource.Token;
-
-    try {
-      logItems = await EventLogManager.GetMostRecentItems(50, cancellationToken);
+    var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken);
+    if (result) {
+      logItems = result.Value;
       agentNamesByGuid = AgentManager.GetAll().ToImmutableDictionary(static kvp => kvp.AgentGuid, static kvp => kvp.Configuration.AgentName);
       instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName);
-    } finally {
-      initializationCancellationTokenSource.Dispose();
+    }
+    else {
+      logItems = ImmutableArray<EventLogItem>.Empty;
+      loadError = result.Error switch {
+        UserActionFailure.NotAuthorized => "You do not have permission to view the event log.",
+        _                               => "Unknown error."
+      };
     }
   }
 
@@ -79,10 +88,4 @@
     };
   }
 
-  protected override void OnDisposed() {
-    try {
-      initializationCancellationTokenSource?.Cancel();
-    } catch (ObjectDisposedException) {}
-  }
-
 }
diff --git a/Web/Phantom.Web/Pages/InstanceDetail.razor b/Web/Phantom.Web/Pages/InstanceDetail.razor
index b232938..752ffae 100644
--- a/Web/Phantom.Web/Pages/InstanceDetail.razor
+++ b/Web/Phantom.Web/Pages/InstanceDetail.razor
@@ -1,9 +1,10 @@
 @page "/instances/{InstanceGuid:guid}"
 @attribute [Authorize(Permission.ViewInstancesPolicy)]
-@using Phantom.Common.Data.Instance
 @using Phantom.Common.Data.Replies
 @using Phantom.Common.Data.Web.Instance
 @using Phantom.Common.Data.Web.Users
+@using Phantom.Utils.Result
+@using Phantom.Common.Data.Instance
 @using Phantom.Web.Services.Instances
 @using Phantom.Web.Services.Authorization
 @inherits Phantom.Web.Components.PhantomComponent
@@ -12,42 +13,42 @@
 @if (Instance == null) {
   <h1>Instance Not Found</h1>
   <p>Return to <a href="instances">all instances</a>.</p>
+  return;
 }
-else {
-  <div class="d-flex flex-row align-items-center gap-3 mb-3">
-    <h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1>
-    <span class="fs-4 text-muted">//</span>
-    <div class="mt-2">
-      <InstanceStatusText Status="Instance.Status" />
-    </div>
-  </div>
-  <div class="d-flex flex-row align-items-center gap-2">
-    <PermissionView Permission="Permission.ControlInstances">
-      <button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button>
-      <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button>
-      <span><!-- extra spacing --></span>
-    </PermissionView>
-    <PermissionView Permission="Permission.CreateInstances">
-      <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a>
-    </PermissionView>
-  </div>
-  @if (lastError != null) {
-    <p class="text-danger mt-2">@lastError</p>
-  }
-
-  <PermissionView Permission="Permission.ViewInstanceLogs">
-    <InstanceLog InstanceGuid="InstanceGuid" />
-  </PermissionView>
 
+<div class="d-flex flex-row align-items-center gap-3 mb-3">
+  <h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1>
+  <span class="fs-4 text-muted">//</span>
+  <div class="mt-2">
+    <InstanceStatusText Status="Instance.Status" />
+  </div>
+</div>
+<div class="d-flex flex-row align-items-center gap-2">
   <PermissionView Permission="Permission.ControlInstances">
-    <div class="mb-3">
-      <InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" />
-    </div>
-
-    <InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" />
+    <button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button>
+    <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button>
+    <span><!-- extra spacing --></span>
   </PermissionView>
+  <PermissionView Permission="Permission.CreateInstances">
+    <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a>
+  </PermissionView>
+</div>
+@if (lastError != null) {
+  <p class="text-danger mt-2">@lastError</p>
 }
 
+<PermissionView Permission="Permission.ViewInstanceLogs">
+  <InstanceLog InstanceGuid="InstanceGuid" />
+</PermissionView>
+
+<PermissionView Permission="Permission.ControlInstances">
+  <div class="mb-3">
+    <InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" />
+  </div>
+
+  <InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" />
+</PermissionView>
+
 @code {
 
   [Parameter]
@@ -73,20 +74,32 @@ else {
     lastError = null;
 
     try {
-      var loggedInUserGuid = await GetUserGuid();
-      if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) {
-        lastError = "You do not have permission to launch instances.";
-        return;
-      }
-
       if (Instance == null) {
         lastError = "Instance not found.";
         return;
       }
 
-      var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken);
-      if (!result.Is(LaunchInstanceResult.LaunchInitiated)) {
-        lastError = result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence);
+      var result = await InstanceManager.LaunchInstance(await GetAuthenticatedUser(), Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken);
+
+      switch (result.Variant()) {
+        case Ok<LaunchInstanceResult>(LaunchInstanceResult.LaunchInitiated):
+          break;
+
+        case Ok<LaunchInstanceResult>(var launchInstanceResult):
+          lastError = launchInstanceResult.ToSentence();
+          break;
+
+        case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)):
+          lastError = failure.ToSentence();
+          break;
+
+        case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)):
+          lastError = "You do not have permission to launch this instance.";
+          break;
+
+        default:
+          lastError = "Unknown error.";
+          break;
       }
     } finally {
       isLaunchingInstance = false;
diff --git a/Web/Phantom.Web/Pages/Users.razor b/Web/Phantom.Web/Pages/Users.razor
index 3dbbe0a..aeedf75 100644
--- a/Web/Phantom.Web/Pages/Users.razor
+++ b/Web/Phantom.Web/Pages/Users.razor
@@ -60,7 +60,7 @@
 
 @code {
 
-  private Guid? me = Guid.Empty;
+  private Guid? me = null;
   private ImmutableArray<UserInfo>? allUsers;
   private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty;
   private readonly Dictionary<Guid, string> userGuidToRoleDescription = new ();
@@ -71,7 +71,7 @@
   private UserDeleteDialog userDeleteDialog = null!;
 
   protected override async Task OnInitializedAsync() {
-    me = await GetUserGuid();
+    me = (await GetAuthenticatedUser())?.Info.Guid;
 
     allUsers = (await UserManager.GetAll(CancellationToken)).Sort(static (a, b) => a.Name.CompareTo(b.Name));
     allRolesByGuid = (await RoleManager.GetAll(CancellationToken)).ToImmutableDictionary(static role => role.Guid, static role => role);
diff --git a/Web/Phantom.Web/Shared/InstanceAddOrEditForm.razor b/Web/Phantom.Web/Shared/InstanceAddOrEditForm.razor
index 8913d78..97a676e 100644
--- a/Web/Phantom.Web/Shared/InstanceAddOrEditForm.razor
+++ b/Web/Phantom.Web/Shared/InstanceAddOrEditForm.razor
@@ -2,13 +2,14 @@
 @using System.Collections.Immutable
 @using System.ComponentModel.DataAnnotations
 @using System.Diagnostics.CodeAnalysis
-@using Phantom.Common.Data.Minecraft
-@using Phantom.Common.Data.Replies
-@using Phantom.Common.Data.Web.Agent
 @using Phantom.Common.Data.Web.Instance
 @using Phantom.Common.Data.Web.Minecraft
 @using Phantom.Common.Data.Web.Users
 @using Phantom.Common.Messages.Web.ToController
+@using Phantom.Utils.Result
+@using Phantom.Common.Data.Replies
+@using Phantom.Common.Data.Web.Agent
+@using Phantom.Common.Data.Minecraft
 @using Phantom.Common.Data.Java
 @using Phantom.Common.Data
 @using Phantom.Common.Data.Instance
@@ -29,13 +30,14 @@
       @{
         static RenderFragment GetAgentOption(Agent agent) {
           var configuration = agent.Configuration;
-          return @<option value="@agent.AgentGuid">
-                   @configuration.AgentName
-                   &bullet;
-                   @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances")
-                   &bullet;
-                   @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(configuration.MaxMemory.InMegabytes) MB RAM
-                 </option>;
+          return
+            @<option value="@agent.AgentGuid">
+              @configuration.AgentName
+              &bullet;
+              @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances")
+              &bullet;
+              @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(configuration.MaxMemory.InMegabytes) MB RAM
+            </option>;
         }
       }
       @if (EditedInstance == null) {
@@ -168,12 +170,12 @@
 
   [Parameter, EditorRequired]
   public Instance? EditedInstance { get; init; }
-  
+
   private ConfigureInstanceFormModel form = null!;
 
   private ImmutableDictionary<Guid, Agent> allAgentsByGuid = ImmutableDictionary<Guid, Agent>.Empty;
   private ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> allAgentJavaRuntimes = ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>.Empty;
-  
+
   private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release;
   private ImmutableArray<MinecraftVersion> allMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty;
   private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty;
@@ -278,7 +280,7 @@
   protected override async Task OnInitializedAsync() {
     var agentJavaRuntimesTask = ControllerConnection.Send<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(new GetAgentJavaRuntimesMessage(), TimeSpan.FromSeconds(30));
     var minecraftVersionsTask = ControllerConnection.Send<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(new GetMinecraftVersionsMessage(), TimeSpan.FromSeconds(30));
-    
+
     allAgentsByGuid = AgentManager.ToDictionaryByGuid();
     allAgentJavaRuntimes = await agentJavaRuntimesTask;
     allMinecraftVersions = await minecraftVersionsTask;
@@ -294,7 +296,7 @@
       form.MemoryUnits = configuration.MemoryAllocation.RawValue;
       form.JavaRuntimeGuid = configuration.JavaRuntimeGuid;
       form.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments);
-      
+
       minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == configuration.MinecraftVersion)?.Type ?? minecraftVersionType;
     }
 
@@ -303,7 +305,7 @@
     form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort));
     form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
     form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
-    
+
     SetMinecraftVersionType(minecraftVersionType);
   }
 
@@ -324,12 +326,6 @@
 
     await form.SubmitModel.StartSubmitting();
 
-    var loggedInUserGuid = await GetUserGuid();
-    if (loggedInUserGuid == null || !await CheckPermission(Permission.CreateInstances)) {
-      form.SubmitModel.StopSubmitting("You do not have permission to edit instances.");
-      return;
-    }
-    
     var instanceGuid = EditedInstance?.InstanceGuid ?? Guid.NewGuid();
     var instanceConfiguration = new InstanceConfiguration(
       EditedInstance?.Configuration.AgentGuid ?? selectedAgent.AgentGuid,
@@ -343,12 +339,28 @@
       JvmArgumentsHelper.Split(form.JvmArguments)
     );
 
-    var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instanceGuid, instanceConfiguration, CancellationToken);
-    if (result.Is(CreateOrUpdateInstanceResult.Success)) {
-      await Navigation.NavigateTo("instances/" + instanceGuid);
-    }
-    else {
-      form.SubmitModel.StopSubmitting(result.Map(CreateOrUpdateInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence));
+    var result = await InstanceManager.CreateOrUpdateInstance(await GetAuthenticatedUser(), instanceGuid, instanceConfiguration, CancellationToken);
+
+    switch (result.Variant()) {
+      case Ok<CreateOrUpdateInstanceResult>(CreateOrUpdateInstanceResult.Success):
+        await Navigation.NavigateTo("instances/" + instanceGuid);
+        break;
+
+      case Ok<CreateOrUpdateInstanceResult>(var createOrUpdateInstanceResult):
+        form.SubmitModel.StopSubmitting(createOrUpdateInstanceResult.ToSentence());
+        break;
+
+      case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)):
+        form.SubmitModel.StopSubmitting(failure.ToSentence());
+        break;
+
+      case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)):
+        form.SubmitModel.StopSubmitting("You do not have permission to create or edit instances.");
+        break;
+
+      default:
+        form.SubmitModel.StopSubmitting("Unknown error.");
+        break;
     }
   }
 
diff --git a/Web/Phantom.Web/Shared/InstanceCommandInput.razor b/Web/Phantom.Web/Shared/InstanceCommandInput.razor
index 1c6b0ab..fd6266b 100644
--- a/Web/Phantom.Web/Shared/InstanceCommandInput.razor
+++ b/Web/Phantom.Web/Shared/InstanceCommandInput.razor
@@ -1,6 +1,7 @@
-@using Phantom.Web.Services.Instances
+@using Phantom.Common.Data.Replies
 @using Phantom.Common.Data.Web.Users
-@using Phantom.Common.Data.Replies
+@using Phantom.Utils.Result
+@using Phantom.Web.Services.Instances
 @inherits Phantom.Web.Components.PhantomComponent
 @inject InstanceManager InstanceManager
 
@@ -18,7 +19,7 @@
 
   [Parameter, EditorRequired]
   public Guid AgentGuid { get; set; }
-  
+
   [Parameter, EditorRequired]
   public Guid InstanceGuid { get; set; }
 
@@ -36,19 +37,29 @@
   private async Task ExecuteCommand(EditContext context) {
     await form.SubmitModel.StartSubmitting();
 
-    var loggedInUserGuid = await GetUserGuid();
-    if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) {
-      form.SubmitModel.StopSubmitting("You do not have permission to execute commands.");
-      return;
-    }
+    var result = await InstanceManager.SendCommandToInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, form.Command, CancellationToken);
 
-    var result = await InstanceManager.SendCommandToInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, form.Command, CancellationToken);
-    if (result.Is(SendCommandToInstanceResult.Success)) {
-      form.Command = string.Empty;
-      form.SubmitModel.StopSubmitting();
-    }
-    else {
-      form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence));
+    switch (result.Variant()) {
+      case Ok<SendCommandToInstanceResult>(SendCommandToInstanceResult.Success):
+        form.Command = string.Empty;
+        form.SubmitModel.StopSubmitting();
+        break;
+
+      case Ok<SendCommandToInstanceResult>(var sendCommandToInstanceResult):
+        form.SubmitModel.StopSubmitting(sendCommandToInstanceResult.ToSentence());
+        break;
+
+      case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)):
+        form.SubmitModel.StopSubmitting(failure.ToSentence());
+        break;
+
+      case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)):
+        form.SubmitModel.StopSubmitting("You do not have permission to send commands to this instance.");
+        break;
+
+      default:
+        form.SubmitModel.StopSubmitting("Unknown error.");
+        break;
     }
 
     StateHasChanged();
diff --git a/Web/Phantom.Web/Shared/InstanceStopDialog.razor b/Web/Phantom.Web/Shared/InstanceStopDialog.razor
index f2ff23c..761888a 100644
--- a/Web/Phantom.Web/Shared/InstanceStopDialog.razor
+++ b/Web/Phantom.Web/Shared/InstanceStopDialog.razor
@@ -1,8 +1,9 @@
-@using Phantom.Web.Services.Instances
-@using System.ComponentModel.DataAnnotations
+@using Phantom.Common.Data.Replies
 @using Phantom.Common.Data.Web.Users
+@using Phantom.Utils.Result
+@using Phantom.Web.Services.Instances
+@using System.ComponentModel.DataAnnotations
 @using Phantom.Common.Data.Minecraft
-@using Phantom.Common.Data.Replies
 @inherits Phantom.Web.Components.PhantomComponent
 @inject IJSRuntime Js;
 @inject InstanceManager InstanceManager;
@@ -33,7 +34,7 @@
 
   [Parameter, EditorRequired]
   public Guid AgentGuid { get; init; }
-  
+
   [Parameter, EditorRequired]
   public Guid InstanceGuid { get; init; }
 
@@ -53,19 +54,29 @@
   private async Task StopInstance(EditContext context) {
     await form.SubmitModel.StartSubmitting();
 
-    var loggedInUserGuid = await GetUserGuid();
-    if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) {
-      form.SubmitModel.StopSubmitting("You do not have permission to stop instances.");
-      return;
-    }
+    var result = await InstanceManager.StopInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken);
 
-    var result = await InstanceManager.StopInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken);
-    if (result.Is(StopInstanceResult.StopInitiated)) {
-      await Js.InvokeVoidAsync("closeModal", ModalId);
-      form.SubmitModel.StopSubmitting();
-    }
-    else {
-      form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence));
+    switch (result.Variant()) {
+      case Ok<StopInstanceResult>(StopInstanceResult.StopInitiated):
+        await Js.InvokeVoidAsync("closeModal", ModalId);
+        form.SubmitModel.StopSubmitting();
+        break;
+
+      case Ok<StopInstanceResult>(var stopInstanceResult):
+        form.SubmitModel.StopSubmitting(stopInstanceResult.ToSentence());
+        break;
+
+      case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)):
+        form.SubmitModel.StopSubmitting(failure.ToSentence());
+        break;
+
+      case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)):
+        form.SubmitModel.StopSubmitting("You do not have permission to stop this instance.");
+        break;
+
+      default:
+        form.SubmitModel.StopSubmitting("Unknown error.");
+        break;
     }
   }
 
diff --git a/Web/Phantom.Web/Shared/UserAddDialog.razor b/Web/Phantom.Web/Shared/UserAddDialog.razor
index 264b678..79e0a6a 100644
--- a/Web/Phantom.Web/Shared/UserAddDialog.razor
+++ b/Web/Phantom.Web/Shared/UserAddDialog.razor
@@ -1,5 +1,6 @@
 @using Phantom.Common.Data.Web.Users
 @using Phantom.Common.Data.Web.Users.CreateUserResults
+@using Phantom.Utils.Result
 @using Phantom.Web.Services.Users
 @using System.ComponentModel.DataAnnotations
 @inherits Phantom.Web.Components.PhantomComponent
@@ -39,7 +40,7 @@
   [Parameter]
   public EventCallback<UserInfo> UserAdded { get; set; }
 
-  private readonly AddUserFormModel form = new();
+  private readonly AddUserFormModel form = new ();
 
   private sealed class AddUserFormModel : FormModel {
     [Required]
@@ -52,23 +53,23 @@
   private async Task AddUser(EditContext context) {
     await form.SubmitModel.StartSubmitting();
 
-    var loggedInUserGuid = await GetUserGuid();
-    if (loggedInUserGuid == null || !await CheckPermission(Permission.EditUsers)) {
-      form.SubmitModel.StopSubmitting("You do not have permission to add users.");
-      return;
-    }
+    var result = await UserManager.Create(await GetAuthenticatedUser(), form.Username, form.Password, CancellationToken);
 
-    switch (await UserManager.Create(loggedInUserGuid.Value, form.Username, form.Password, CancellationToken)) {
-      case Success success:
+    switch (result.Variant()) {
+      case Ok<CreateUserResult>(Success success):
         await UserAdded.InvokeAsync(success.User);
         await Js.InvokeVoidAsync("closeModal", ModalId);
         form.SubmitModel.StopSubmitting();
         break;
 
-      case CreationFailed fail:
+      case Ok<CreateUserResult>(CreationFailed fail):
         form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n"));
         break;
 
+      case Err<UserActionFailure>(UserActionFailure.NotAuthorized):
+        form.SubmitModel.StopSubmitting("You do not have permission to add users.");
+        break;
+
       default:
         form.SubmitModel.StopSubmitting("Unknown error.");
         break;
diff --git a/Web/Phantom.Web/Shared/UserDeleteDialog.razor b/Web/Phantom.Web/Shared/UserDeleteDialog.razor
index 78440e9..b87dde4 100644
--- a/Web/Phantom.Web/Shared/UserDeleteDialog.razor
+++ b/Web/Phantom.Web/Shared/UserDeleteDialog.razor
@@ -1,4 +1,5 @@
 @using Phantom.Common.Data.Web.Users
+@using Phantom.Web.Services.Authentication
 @using Phantom.Web.Services.Users
 @inherits UserEditDialogBase
 @inject UserManager UserManager
@@ -17,8 +18,13 @@
 
 @code {
 
-  protected override async Task DoEdit(Guid loggedInUserGuid, UserInfo user) {
-    switch (await UserManager.DeleteByGuid(loggedInUserGuid, user.Guid, CancellationToken)) {
+  protected override async Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser) {
+    var result = await UserManager.DeleteByGuid(authenticatedUser, editedUser.Guid, CancellationToken);
+    if (!result) {
+      return result.Error;
+    }
+    
+    switch (result.Value) {
       case DeleteUserResult.Deleted:
       case DeleteUserResult.NotFound:
         await OnEditSuccess();
@@ -28,6 +34,8 @@
         OnEditFailure("Could not delete user.");
         break;
     }
+    
+    return null;
   }
 
 }
diff --git a/Web/Phantom.Web/Shared/UserEditDialogBase.cs b/Web/Phantom.Web/Shared/UserEditDialogBase.cs
index be676f2..c2a564c 100644
--- a/Web/Phantom.Web/Shared/UserEditDialogBase.cs
+++ b/Web/Phantom.Web/Shared/UserEditDialogBase.cs
@@ -3,6 +3,7 @@ using Microsoft.JSInterop;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Web.Components;
 using Phantom.Web.Components.Forms;
+using Phantom.Web.Services.Authentication;
 
 namespace Phantom.Web.Shared;
 
@@ -16,7 +17,7 @@ public abstract class UserEditDialogBase : PhantomComponent {
 	[Parameter]
 	public EventCallback<UserInfo> UserModified { get; set; }
 
-	protected readonly FormButtonSubmit.SubmitModel SubmitModel = new();
+	protected readonly FormButtonSubmit.SubmitModel SubmitModel = new ();
 
 	private UserInfo? EditedUser { get; set; } = null;
 	protected string EditedUserName { get; private set; } = string.Empty;
@@ -41,19 +42,26 @@ public abstract class UserEditDialogBase : PhantomComponent {
 	protected async Task Submit() {
 		await SubmitModel.StartSubmitting();
 
-		var loggedInUserGuid = await GetUserGuid();
-		if (loggedInUserGuid == null || !await CheckPermission(Permission.EditUsers)) {
-			SubmitModel.StopSubmitting("You do not have permission to edit users.");
-		}
-		else if (EditedUser == null) {
+		if (EditedUser == null) {
 			SubmitModel.StopSubmitting("Invalid user.");
+			return;
 		}
-		else {
-			await DoEdit(loggedInUserGuid.Value, EditedUser);
+
+		switch (await DoEdit(await GetAuthenticatedUser(), EditedUser)) {
+			case null:
+				break;
+
+			case UserActionFailure.NotAuthorized:
+				SubmitModel.StopSubmitting("You do not have permission to edit users.");
+				break;
+
+			default:
+				SubmitModel.StopSubmitting("Unknown error.");
+				break;
 		}
 	}
-	
-	protected abstract Task DoEdit(Guid loggedInUserGuid, UserInfo user);
+
+	protected abstract Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser);
 
 	protected async Task OnEditSuccess() {
 		await UserModified.InvokeAsync(EditedUser);
@@ -61,7 +69,7 @@ public abstract class UserEditDialogBase : PhantomComponent {
 		SubmitModel.StopSubmitting();
 		OnClosed();
 	}
-	
+
 	protected void OnEditFailure(string message) {
 		SubmitModel.StopSubmitting(message);
 	}
diff --git a/Web/Phantom.Web/Shared/UserRolesDialog.razor b/Web/Phantom.Web/Shared/UserRolesDialog.razor
index 046df3a..a89cab3 100644
--- a/Web/Phantom.Web/Shared/UserRolesDialog.razor
+++ b/Web/Phantom.Web/Shared/UserRolesDialog.razor
@@ -1,5 +1,6 @@
 @using System.Collections.Immutable
 @using Phantom.Common.Data.Web.Users
+@using Phantom.Web.Services.Authentication
 @using Phantom.Web.Services.Users
 @inherits UserEditDialogBase
 @inject RoleManager RoleManager
@@ -36,8 +37,8 @@
     this.items = allRoles.Select(role => new RoleItem(role, currentRoleGuids.Contains(role.Guid))).ToList();
   }
 
-  protected override async Task DoEdit(Guid loggedInUserGuid, UserInfo user) {
-    var currentRoleGuids = await UserRoleManager.GetUserRoles(user.Guid, CancellationToken);
+  protected override async Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser) {
+    var currentRoleGuids = await UserRoleManager.GetUserRoles(editedUser.Guid, CancellationToken);
     var addToRoleGuids = ImmutableHashSet.CreateBuilder<Guid>();
     var removeFromRoleGuids = ImmutableHashSet.CreateBuilder<Guid>();
 
@@ -56,18 +57,21 @@
       }
     }
     
-    await DoChangeUserRoles(user, loggedInUserGuid, addToRoleGuids.ToImmutable(), removeFromRoleGuids.ToImmutable());
+    return await DoChangeUserRoles(authenticatedUser, editedUser, addToRoleGuids.ToImmutable(), removeFromRoleGuids.ToImmutable());
   }
 
-  private async Task DoChangeUserRoles(UserInfo user, Guid loggedInUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) {
-    var result = await UserRoleManager.ChangeUserRoles(loggedInUserGuid, user.Guid, addToRoleGuids, removeFromRoleGuids, CancellationToken);
+  private async Task<UserActionFailure?> DoChangeUserRoles(AuthenticatedUser? authenticatedUser, UserInfo editedUser, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) {
+    var result = await UserRoleManager.ChangeUserRoles(authenticatedUser, editedUser.Guid, addToRoleGuids, removeFromRoleGuids, CancellationToken);
+    if (!result) {
+      return result.Error;
+    }
     
-    var failedToAdd = addToRoleGuids.Except(result.AddedToRoleGuids);
-    var failedToRemove = removeFromRoleGuids.Except(result.RemovedFromRoleGuids);
+    var failedToAdd = addToRoleGuids.Except(result.Value.AddedToRoleGuids);
+    var failedToRemove = removeFromRoleGuids.Except(result.Value.RemovedFromRoleGuids);
     
     if (failedToAdd.IsEmpty && failedToRemove.IsEmpty) {
       await OnEditSuccess();
-      return;
+      return null;
     }
     
     var errors = new List<string>();
@@ -81,6 +85,7 @@
     }
     
     OnEditFailure(string.Join("\n", errors));
+    return null;
   }
 
   private string GetRoleName(Guid roleGuid) {