diff --git a/Common/Phantom.Common.Data.Web/Users/AuthenticatedUserInfo.cs b/Common/Phantom.Common.Data.Web/Users/AuthenticatedUserInfo.cs
new file mode 100644
index 0000000..7db483b
--- /dev/null
+++ b/Common/Phantom.Common.Data.Web/Users/AuthenticatedUserInfo.cs
@@ -0,0 +1,10 @@
+using MemoryPack;
+
+namespace Phantom.Common.Data.Web.Users;
+
+[MemoryPackable(GenerateType.VersionTolerant)]
+public sealed partial record AuthenticatedUserInfo(
+	[property: MemoryPackOrder(0)] Guid Guid,
+	[property: MemoryPackOrder(1)] string Name,
+	[property: MemoryPackOrder(2)] PermissionSet Permissions
+);
diff --git a/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs b/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs
index 172e9b5..28d12c0 100644
--- a/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs
+++ b/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs
@@ -5,7 +5,6 @@ namespace Phantom.Common.Data.Web.Users;
 
 [MemoryPackable(GenerateType.VersionTolerant)]
 public sealed partial record LogInSuccess(
-	[property: MemoryPackOrder(0)] Guid UserGuid,
-	[property: MemoryPackOrder(1)] PermissionSet Permissions,
-	[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
+	[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo,
+	[property: MemoryPackOrder(1)] ImmutableArray<byte> Token
 );
diff --git a/Common/Phantom.Common.Data/Optional.cs b/Common/Phantom.Common.Data/Optional.cs
new file mode 100644
index 0000000..bc93a5f
--- /dev/null
+++ b/Common/Phantom.Common.Data/Optional.cs
@@ -0,0 +1,10 @@
+using MemoryPack;
+
+namespace Phantom.Common.Data;
+
+[MemoryPackable]
+public readonly partial record struct Optional<T>(T? Value) {
+	public static implicit operator Optional<T>(T? value) {
+		return new Optional<T>(value);
+	}
+}
diff --git a/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs b/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs
new file mode 100644
index 0000000..27ccec9
--- /dev/null
+++ b/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs
@@ -0,0 +1,13 @@
+using System.Collections.Immutable;
+using MemoryPack;
+using Phantom.Common.Data;
+using Phantom.Common.Data.Web.Users;
+using Phantom.Utils.Actor;
+
+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
+) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>;
diff --git a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
index bf48c78..f1aa4b5 100644
--- a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
+++ b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
@@ -1,4 +1,5 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Java;
 using Phantom.Common.Data.Minecraft;
 using Phantom.Common.Data.Replies;
@@ -25,21 +26,22 @@ public static class WebMessageRegistries {
 		ToController.Add<UnregisterWebMessage>(1);
 		ToController.Add<LogInMessage, LogInSuccess?>(2);
 		ToController.Add<LogOutMessage>(3);
-		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(4);
-		ToController.Add<CreateUserMessage, CreateUserResult>(5);
-		ToController.Add<DeleteUserMessage, DeleteUserResult>(6);
-		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(7);
-		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(8);
-		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(9);
-		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(10);
-		ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(11);
-		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(12);
-		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(13);
-		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(14);
-		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(15);
-		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(16);
-		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(17);
-		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(18);
+		ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4);
+		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5);
+		ToController.Add<CreateUserMessage, CreateUserResult>(6);
+		ToController.Add<DeleteUserMessage, DeleteUserResult>(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, InstanceActionResult<CreateOrUpdateInstanceResult>>(12);
+		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(13);
+		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(14);
+		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(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<ReplyMessage>(127);
 		
 		ToWeb.Add<RegisterWebResultMessage>(0);
diff --git a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
index d429b4d..7f64630 100644
--- a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
+++ b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
@@ -1,4 +1,5 @@
 using System.Collections.Immutable;
+using Phantom.Common.Data;
 using Phantom.Common.Data.Java;
 using Phantom.Common.Data.Minecraft;
 using Phantom.Common.Data.Replies;
@@ -71,6 +72,7 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 		Receive<UnregisterWebMessage>(HandleUnregisterWeb);
 		ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
 		Receive<LogOutMessage>(HandleLogOut);
+		ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
 		ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
 		ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
 		ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
@@ -104,7 +106,11 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 	private void HandleLogOut(LogOutMessage message) {
 		_ = userLoginManager.LogOut(message.UserGuid, message.SessionToken);
 	}
-	
+
+	private Optional<AuthenticatedUserInfo> GetAuthenticatedUser(GetAuthenticatedUser message) {
+		return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.SessionToken);
+	}
+    
 	private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
 		return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
 	}
diff --git a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs
index e9f27cc..6584908 100644
--- a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs
+++ b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs
@@ -1,5 +1,6 @@
 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;
@@ -9,7 +10,7 @@ namespace Phantom.Controller.Services.Users;
 
 sealed class UserLoginManager {
 	private const int SessionIdBytes = 20;
-	private readonly ConcurrentDictionary<Guid, List<ImmutableArray<byte>>> sessionTokensByUserGuid = new ();
+	private readonly ConcurrentDictionary<Guid, UserSession> sessionsByUserGuid = new ();
 	
 	private readonly UserManager userManager;
 	private readonly PermissionManager permissionManager;
@@ -27,11 +28,11 @@ sealed class UserLoginManager {
 			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));
-		var sessionTokens = sessionTokensByUserGuid.GetOrAdd(user.UserGuid, static _ => new List<ImmutableArray<byte>>());
-		lock (sessionTokens) {
-			sessionTokens.Add(token);
-		}
+		
+		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);
@@ -40,18 +41,18 @@ sealed class UserLoginManager {
 			await db.Ctx.SaveChangesAsync();
 		}
 
-		return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token);
+		return new LogInSuccess(userInfo, token);
 	}
 
-	public async Task LogOut(Guid userGuid, ImmutableArray<byte> sessionToken) {
-		if (!sessionTokensByUserGuid.TryGetValue(userGuid, out var sessionTokens)) {
-			return;
-		}
-
-		lock (sessionTokens) {
-			if (sessionTokens.RemoveAll(token => token.SequenceEqual(sessionToken)) == 0) {
+	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();
@@ -61,4 +62,46 @@ sealed class UserLoginManager {
 			
 		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/Web/Phantom.Web.Components/PhantomComponent.cs b/Web/Phantom.Web.Components/PhantomComponent.cs
index 42f80f3..d593732 100644
--- a/Web/Phantom.Web.Components/PhantomComponent.cs
+++ b/Web/Phantom.Web.Components/PhantomComponent.cs
@@ -2,9 +2,8 @@
 using Microsoft.AspNetCore.Components.Authorization;
 using Phantom.Common.Data.Web.Users;
 using Phantom.Utils.Logging;
-using Phantom.Web.Services.Authorization;
+using Phantom.Web.Services.Authentication;
 using ILogger = Serilog.ILogger;
-using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
 
 namespace Phantom.Web.Components;
 
@@ -14,21 +13,18 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
 	[CascadingParameter]
 	public Task<AuthenticationState> AuthenticationStateTask { get; set; } = null!;
 
-	[Inject]
-	public PermissionManager PermissionManager { get; set; } = null!;
-
 	private readonly CancellationTokenSource cancellationTokenSource = new ();
 
 	protected CancellationToken CancellationToken => cancellationTokenSource.Token;
 
 	protected async Task<Guid?> GetUserGuid() {
 		var authenticationState = await AuthenticationStateTask;
-		return UserInfo.TryGetGuid(authenticationState.User);
+		return authenticationState.TryGetGuid();
 	}
 
 	protected async Task<bool> CheckPermission(Permission permission) {
 		var authenticationState = await AuthenticationStateTask;
-		return PermissionManager.CheckPermission(authenticationState.User, permission);
+		return authenticationState.CheckPermission(permission);
 	}
 
 	protected void InvokeAsyncChecked(Func<Task> task) {
diff --git a/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs b/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs
new file mode 100644
index 0000000..18e7907
--- /dev/null
+++ b/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs
@@ -0,0 +1,27 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Components.Authorization;
+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 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 PermissionSet GetPermissions(this AuthenticationState authenticationState) {
+		return authenticationState.User.GetPermissions();
+	}
+
+	public static bool CheckPermission(this AuthenticationState authenticationState, Permission permission) {
+		return authenticationState.User.CheckPermission(permission);
+	}
+}
diff --git a/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs b/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs
index a53df81..c2ae175 100644
--- a/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs
+++ b/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs
@@ -1,26 +1,30 @@
 using System.Security.Claims;
 using Microsoft.AspNetCore.Components.Authorization;
 using Microsoft.AspNetCore.Components.Server;
+using Phantom.Common.Data;
+using Phantom.Common.Data.Web.Users;
+using Phantom.Common.Messages.Web.ToController;
+using Phantom.Web.Services.Rpc;
 
 namespace Phantom.Web.Services.Authentication;
 
 public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider {
-	private readonly UserSessionManager sessionManager;
 	private readonly UserSessionBrowserStorage sessionBrowserStorage;
+	private readonly ControllerConnection controllerConnection;
 	private bool isLoaded;
 
-	public CustomAuthenticationStateProvider(UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage) {
-		this.sessionManager = sessionManager;
+	public CustomAuthenticationStateProvider(UserSessionBrowserStorage sessionBrowserStorage, ControllerConnection controllerConnection) {
 		this.sessionBrowserStorage = sessionBrowserStorage;
+		this.controllerConnection = controllerConnection;
 	}
 
 	public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
 		if (!isLoaded) {
 			var stored = await sessionBrowserStorage.Get();
 			if (stored != null) {
-				var session = sessionManager.FindWithToken(stored.UserGuid, stored.Token);
-				if (session != null) {
-					SetLoadedSession(session);
+				var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, stored.Token), TimeSpan.FromSeconds(30));
+				if (session.Value is {} userInfo) {
+					SetLoadedSession(userInfo);
 				}
 			}
 		}
@@ -28,9 +32,9 @@ public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStat
 		return await base.GetAuthenticationStateAsync();
 	}
 
-	internal void SetLoadedSession(UserInfo user) {
+	internal void SetLoadedSession(AuthenticatedUserInfo user) {
 		isLoaded = true;
-		SetAuthenticationState(Task.FromResult(new AuthenticationState(user.AsClaimsPrincipal)));
+		SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(user))));
 	}
 
 	internal void SetUnloadedSession() {
diff --git a/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs b/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs
new file mode 100644
index 0000000..3a0188d
--- /dev/null
+++ b/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs
@@ -0,0 +1,18 @@
+using System.Security.Claims;
+using Phantom.Common.Data.Web.Users;
+
+namespace Phantom.Web.Services.Authentication;
+
+sealed class CustomClaimsPrincipal : ClaimsPrincipal {
+	internal AuthenticatedUserInfo UserInfo { get; }
+
+	internal CustomClaimsPrincipal(AuthenticatedUserInfo userInfo) : base(GetIdentity(userInfo)) {
+		UserInfo = userInfo;
+	}
+
+	private static ClaimsIdentity GetIdentity(AuthenticatedUserInfo userInfo) {
+		var identity = new ClaimsIdentity("Phantom");
+		identity.AddClaim(new Claim(ClaimTypes.Name, userInfo.Name));
+		return identity;
+	}
+}
diff --git a/Web/Phantom.Web.Services/Authentication/UserInfo.cs b/Web/Phantom.Web.Services/Authentication/UserInfo.cs
deleted file mode 100644
index ceb441d..0000000
--- a/Web/Phantom.Web.Services/Authentication/UserInfo.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Security.Claims;
-using Phantom.Common.Data.Web.Users;
-
-namespace Phantom.Web.Services.Authentication;
-
-public sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Permissions) {
-	private const string AuthenticationType = "Phantom";
-
-	internal ClaimsPrincipal AsClaimsPrincipal {
-		get {
-			var identity = new ClaimsIdentity(AuthenticationType);
-
-			identity.AddClaim(new Claim(ClaimTypes.Name, Username));
-			identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, UserGuid.ToString()));
-
-			return new ClaimsPrincipal(identity);
-		}
-	}
-
-	public static Guid? TryGetGuid(ClaimsPrincipal principal) {
-		return principal.Identity is { IsAuthenticated: true, AuthenticationType: AuthenticationType } && principal.FindFirstValue(ClaimTypes.NameIdentifier) is {} guidStr && Guid.TryParse(guidStr, out var guid) ? guid : null;
-	}
-}
diff --git a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
index f9d27d4..6a1c914 100644
--- a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
+++ b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
@@ -10,14 +10,12 @@ public sealed class UserLoginManager {
 	private static readonly ILogger Logger = PhantomLogger.Create<UserLoginManager>();
 
 	private readonly Navigation navigation;
-	private readonly UserSessionManager sessionManager;
 	private readonly UserSessionBrowserStorage sessionBrowserStorage;
 	private readonly CustomAuthenticationStateProvider authenticationStateProvider;
 	private readonly ControllerConnection controllerConnection;
 
-	public UserLoginManager(Navigation navigation, UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage, CustomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) {
+	public UserLoginManager(Navigation navigation, UserSessionBrowserStorage sessionBrowserStorage, CustomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) {
 		this.navigation = navigation;
-		this.sessionManager = sessionManager;
 		this.sessionBrowserStorage = sessionBrowserStorage;
 		this.authenticationStateProvider = authenticationStateProvider;
 		this.controllerConnection = controllerConnection;
@@ -38,13 +36,9 @@ public sealed class UserLoginManager {
 
 		Logger.Information("Successfully logged in {Username}.", username);
 
-		var userGuid = success.UserGuid;
-		var userInfo = new UserInfo(userGuid, username, success.Permissions);
-		var token = success.Token;
+		var userInfo = success.UserInfo;
 
-		await sessionBrowserStorage.Store(userGuid, token);
-		sessionManager.Add(userInfo, token);
-		
+		await sessionBrowserStorage.Store(userInfo.Guid, success.Token);
 		authenticationStateProvider.SetLoadedSession(userInfo);
 		await navigation.NavigateTo(returnUrl ?? string.Empty);
 		
@@ -53,7 +47,7 @@ public sealed class UserLoginManager {
 
 	public async Task LogOut() {
 		var stored = await sessionBrowserStorage.Delete();
-		if (stored != null && sessionManager.Remove(stored.UserGuid, stored.Token)) {
+		if (stored != null) {
 			await controllerConnection.Send(new LogOutMessage(stored.UserGuid, stored.Token));
 		}
 
diff --git a/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs b/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs
deleted file mode 100644
index 3c830a1..0000000
--- a/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Collections.Concurrent;
-using System.Collections.Immutable;
-
-namespace Phantom.Web.Services.Authentication;
-
-public sealed class UserSessionManager {
-	private readonly ConcurrentDictionary<Guid, UserSessions> userSessions = new ();
-
-	internal void Add(UserInfo user, ImmutableArray<byte> token) {
-		userSessions.AddOrUpdate(
-			user.UserGuid,
-			static (_, u) => new UserSessions(u),
-			static (_, sessions, u) => sessions.WithUserInfo(u),
-			user
-		).AddToken(token);
-	}
-
-	internal UserInfo? Find(Guid userGuid) {
-		return userSessions.TryGetValue(userGuid, out var sessions) ? sessions.UserInfo : null;
-	}
-	
-	internal UserInfo? FindWithToken(Guid userGuid, ImmutableArray<byte> token) {
-		return userSessions.TryGetValue(userGuid, out var sessions) && sessions.HasToken(token) ? sessions.UserInfo : null;
-	}
-
-	internal bool Remove(Guid userGuid, ImmutableArray<byte> token) {
-		if (userSessions.TryGetValue(userGuid, out var sessions)) {
-			sessions.RemoveToken(token);
-			return true;
-		}
-		else {
-			return false;
-		}
-	}
-}
diff --git a/Web/Phantom.Web.Services/Authentication/UserSessions.cs b/Web/Phantom.Web.Services/Authentication/UserSessions.cs
deleted file mode 100644
index 64a82a8..0000000
--- a/Web/Phantom.Web.Services/Authentication/UserSessions.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Collections.Immutable;
-using System.Security.Cryptography;
-
-namespace Phantom.Web.Services.Authentication; 
-
-sealed class UserSessions {
-	public UserInfo UserInfo { get; }
-	
-	private readonly List<ImmutableArray<byte>> tokens = new ();
-	
-	public UserSessions(UserInfo userInfo) {
-		UserInfo = userInfo;
-	}
-
-	private UserSessions(UserInfo userInfo, List<ImmutableArray<byte>> tokens) : this(userInfo) {
-		this.tokens.AddRange(tokens);
-	}
-
-	public UserSessions WithUserInfo(UserInfo user) {
-		List<ImmutableArray<byte>> tokensCopy;
-		lock (tokens) {
-			tokensCopy = new List<ImmutableArray<byte>>(tokens);
-		}
-		
-		return new UserSessions(user, tokensCopy);
-	}
-
-	public void AddToken(ImmutableArray<byte> token) {
-		lock (tokens) {
-			if (!HasToken(token)) {
-				tokens.Add(token);
-			}
-		}
-	}
-
-	public bool HasToken(ImmutableArray<byte> token) {
-		return FindTokenIndex(token) != -1;
-	}
-
-	private int FindTokenIndex(ImmutableArray<byte> token) {
-		lock (tokens) {
-			return tokens.FindIndex(t => CryptographicOperations.FixedTimeEquals(t.AsSpan(), token.AsSpan()));
-		}
-	}
-
-	public void RemoveToken(ImmutableArray<byte> token) {
-		lock (tokens) {
-			int index = FindTokenIndex(token);
-			if (index != -1) {
-				tokens.RemoveAt(index);
-			}
-		}
-	}
-}
diff --git a/Web/Phantom.Web.Services/Authorization/PermissionBasedPolicyHandler.cs b/Web/Phantom.Web.Services/Authorization/PermissionBasedPolicyHandler.cs
index 2e7e976..c2c08cc 100644
--- a/Web/Phantom.Web.Services/Authorization/PermissionBasedPolicyHandler.cs
+++ b/Web/Phantom.Web.Services/Authorization/PermissionBasedPolicyHandler.cs
@@ -1,16 +1,11 @@
 using Microsoft.AspNetCore.Authorization;
+using Phantom.Web.Services.Authentication;
 
 namespace Phantom.Web.Services.Authorization;
 
 sealed class PermissionBasedPolicyHandler : AuthorizationHandler<PermissionBasedPolicyRequirement> {
-	private readonly PermissionManager permissionManager;
-
-	public PermissionBasedPolicyHandler(PermissionManager permissionManager) {
-		this.permissionManager = permissionManager;
-	}
-
 	protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionBasedPolicyRequirement requirement) {
-		if (permissionManager.CheckPermission(context.User, requirement.Permission)) {
+		if (context.User.CheckPermission(requirement.Permission)) {
 			context.Succeed(requirement);
 		}
 		else {
diff --git a/Web/Phantom.Web.Services/Authorization/PermissionManager.cs b/Web/Phantom.Web.Services/Authorization/PermissionManager.cs
deleted file mode 100644
index 5d5d953..0000000
--- a/Web/Phantom.Web.Services/Authorization/PermissionManager.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Security.Claims;
-using Phantom.Common.Data.Web.Users;
-using Phantom.Web.Services.Authentication;
-using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
-
-namespace Phantom.Web.Services.Authorization;
-
-public sealed class PermissionManager {
-	private readonly UserSessionManager sessionManager;
-	
-	public PermissionManager(UserSessionManager sessionManager) {
-		this.sessionManager = sessionManager;
-	}
-
-	public PermissionSet GetPermissions(ClaimsPrincipal user) {
-		return UserInfo.TryGetGuid(user) is {} guid && sessionManager.Find(guid) is {} info ? info.Permissions : PermissionSet.None;
-	}
-	
-	public bool CheckPermission(ClaimsPrincipal user, Permission permission) {
-		return GetPermissions(user).Check(permission);
-	}
-}
diff --git a/Web/Phantom.Web.Services/Authorization/PermissionView.razor b/Web/Phantom.Web.Services/Authorization/PermissionView.razor
index 23638f6..b205335 100644
--- a/Web/Phantom.Web.Services/Authorization/PermissionView.razor
+++ b/Web/Phantom.Web.Services/Authorization/PermissionView.razor
@@ -1,10 +1,9 @@
 @using Microsoft.AspNetCore.Components.Authorization
 @using Phantom.Common.Data.Web.Users
-@inject PermissionManager PermissionManager
-
+@using Phantom.Web.Services.Authentication
 <AuthorizeView>
   <Authorized>
-    @if (ChildContent != null && PermissionManager.CheckPermission(context.User, Permission)) {
+    @if (ChildContent != null && context.CheckPermission(Permission)) {
       @ChildContent(context)
     }
   </Authorized>
diff --git a/Web/Phantom.Web.Services/PhantomWebServices.cs b/Web/Phantom.Web.Services/PhantomWebServices.cs
index 374eed4..cb50001 100644
--- a/Web/Phantom.Web.Services/PhantomWebServices.cs
+++ b/Web/Phantom.Web.Services/PhantomWebServices.cs
@@ -22,7 +22,6 @@ public static class PhantomWebServices {
 		services.AddSingleton<EventLogManager>();
 		
 		services.AddSingleton<UserManager>();
-		services.AddSingleton<UserSessionManager>();
 		services.AddSingleton<AuditLogManager>();
 		services.AddScoped<UserLoginManager>();
 		services.AddScoped<UserSessionBrowserStorage>();
@@ -34,7 +33,6 @@ public static class PhantomWebServices {
 		services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<CustomAuthenticationStateProvider>());
 		
 		services.AddAuthorization(ConfigureAuthorization);
-		services.AddSingleton<PermissionManager>();
 		services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>();
 	}
 	
diff --git a/Web/Phantom.Web/Layout/NavMenu.razor b/Web/Phantom.Web/Layout/NavMenu.razor
index e92cf1b..d071e3a 100644
--- a/Web/Phantom.Web/Layout/NavMenu.razor
+++ b/Web/Phantom.Web/Layout/NavMenu.razor
@@ -1,8 +1,7 @@
-@using Phantom.Web.Services
-@using Phantom.Web.Services.Authorization
+@using Phantom.Web.Services.Authentication
+@using Phantom.Web.Services
 @using Phantom.Common.Data.Web.Users
 @inject ApplicationProperties ApplicationProperties
-@inject PermissionManager PermissionManager
 
 <div class="navbar navbar-dark">
   <div class="container-fluid">
@@ -24,7 +23,7 @@
         <NavMenuItem Label="Login" Icon="account-login" Href="login" />
       </NotAuthorized>
       <Authorized>
-        @{ var permissions = PermissionManager.GetPermissions(context.User); }
+        @{ var permissions = context.GetPermissions(); }
         
         @if (permissions.Check(Permission.ViewInstances)) {
           <NavMenuItem Label="Instances" Icon="folder" Href="instances" />
diff --git a/Web/Phantom.Web/Pages/Users.razor b/Web/Phantom.Web/Pages/Users.razor
index 8d82028..3dbbe0a 100644
--- a/Web/Phantom.Web/Pages/Users.razor
+++ b/Web/Phantom.Web/Pages/Users.razor
@@ -1,9 +1,10 @@
 @page "/users"
 @attribute [Authorize(Permission.ViewUsersPolicy)]
 @using System.Collections.Immutable
-@using Phantom.Common.Data.Web.Users
-@using Phantom.Web.Services.Users
+@using Phantom.Web.Services.Authentication
 @using Phantom.Web.Services.Authorization
+@using Phantom.Web.Services.Users
+@using Phantom.Common.Data.Web.Users
 @inherits Phantom.Web.Components.PhantomComponent
 @inject UserManager UserManager
 @inject RoleManager RoleManager
@@ -17,7 +18,7 @@
 
 <AuthorizeView>
   <Authorized>
-    @{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); }
+    @{ var canEdit = context.CheckPermission(Permission.EditUsers); }
     <Table TItem="UserInfo" Items="allUsers">
       <HeaderRow>
         <Column>Username</Column>