diff --git a/DysonNetwork.Sphere/Auth/OpenId/ConnectionController.cs b/DysonNetwork.Sphere/Auth/OpenId/ConnectionController.cs
index ecaad6f..b9114d0 100644
--- a/DysonNetwork.Sphere/Auth/OpenId/ConnectionController.cs
+++ b/DysonNetwork.Sphere/Auth/OpenId/ConnectionController.cs
@@ -2,7 +2,7 @@ using DysonNetwork.Sphere.Account;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
-using DysonNetwork.Sphere.Auth.OpenId;
+using DysonNetwork.Sphere.Storage;
 using NodaTime;
 
 namespace DysonNetwork.Sphere.Auth.OpenId;
@@ -14,9 +14,13 @@ public class ConnectionController(
     AppDatabase db,
     IEnumerable<OidcService> oidcServices,
     AccountService accounts,
-    AuthService auth
+    AuthService auth,
+    ICacheService cacheService
 ) : ControllerBase
 {
+    private const string StateCachePrefix = "oidc-state:";
+    private const string ReturnUrlCachePrefix = "oidc-returning:";
+    private static readonly TimeSpan StateExpiration = TimeSpan.FromMinutes(15);
     [HttpGet]
     public async Task<ActionResult<List<AccountConnection>>> GetConnections()
     {
@@ -142,10 +146,12 @@ public class ConnectionController(
 
         var state = Guid.NewGuid().ToString("N");
         var nonce = Guid.NewGuid().ToString("N");
-        HttpContext.Session.SetString($"oidc_state_{state}", $"{currentUser.Id}|{request.Provider}|{nonce}");
-
+        var stateValue = $"{currentUser.Id}|{request.Provider}|{nonce}";
         var finalReturnUrl = !string.IsNullOrEmpty(request.ReturnUrl) ? request.ReturnUrl : "/settings/connections";
-        HttpContext.Session.SetString($"oidc_return_url_{state}", finalReturnUrl);
+
+        // Store state and return URL in cache
+        await cacheService.SetAsync($"{StateCachePrefix}{state}", stateValue, StateExpiration);
+        await cacheService.SetAsync($"{ReturnUrlCachePrefix}{state}", finalReturnUrl, StateExpiration);
 
         var authUrl = oidcService.GetAuthorizationUrl(state, nonce);
 
@@ -169,14 +175,22 @@ public class ConnectionController(
         if (callbackData.State == null)
             return BadRequest("State parameter is missing.");
 
-        var sessionState = HttpContext.Session.GetString($"oidc_state_{callbackData.State!}");
-        HttpContext.Session.Remove($"oidc_state_{callbackData.State}");
+        // Get and validate state from cache
+        var stateKey = $"{StateCachePrefix}{callbackData.State}";
+        var stateValue = await cacheService.GetAsync<string>(stateKey);
+        if (string.IsNullOrEmpty(stateValue))
+        {
+            return BadRequest("Invalid or expired state parameter");
+        }
 
-        // If sessionState is present, it's a manual connection flow for an existing user.
-        if (sessionState == null) return await HandleLoginOrRegistration(provider, oidcService, callbackData);
-        var stateParts = sessionState.Split('|');
-        if (stateParts.Length != 3 || !stateParts[1].Equals(provider, StringComparison.OrdinalIgnoreCase))
-            return BadRequest("State mismatch.");
+        // Remove state from cache to prevent replay attacks
+        await cacheService.RemoveAsync(stateKey);
+
+        var stateParts = stateValue.Split('|');
+        if (stateParts.Length != 3)
+        {
+            return BadRequest("Invalid state format");
+        }
 
         var accountId = Guid.Parse(stateParts[0]);
         return await HandleManualConnection(provider, oidcService, callbackData, accountId);
@@ -259,9 +273,9 @@ public class ConnectionController(
         }
 
         // Clean up and redirect
-        var returnUrl = HttpContext.Session.GetString($"oidc_return_url_{callbackData.State}");
-        HttpContext.Session.Remove($"oidc_return_url_{callbackData.State}");
-        HttpContext.Session.Remove($"oidc_state_{callbackData.State}");
+        var returnUrlKey = $"{ReturnUrlCachePrefix}{callbackData.State}";
+        var returnUrl = await cacheService.GetAsync<string>(returnUrlKey);
+        await cacheService.RemoveAsync(returnUrlKey);
 
         return Redirect(string.IsNullOrEmpty(returnUrl) ? "/settings/connections" : returnUrl);
     }
diff --git a/DysonNetwork.Sphere/Auth/OpenId/OidcController.cs b/DysonNetwork.Sphere/Auth/OpenId/OidcController.cs
index ada8bc1..a89d28f 100644
--- a/DysonNetwork.Sphere/Auth/OpenId/OidcController.cs
+++ b/DysonNetwork.Sphere/Auth/OpenId/OidcController.cs
@@ -1,4 +1,5 @@
 using DysonNetwork.Sphere.Account;
+using DysonNetwork.Sphere.Storage;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.IdentityModel.Tokens;
@@ -12,12 +13,16 @@ public class OidcController(
     IServiceProvider serviceProvider,
     AppDatabase db,
     AccountService accounts,
-    AuthService authService
+    AuthService auth,
+    ICacheService cache
 )
     : ControllerBase
 {
+    private const string StateCachePrefix = "oidc-state:";
+    private static readonly TimeSpan StateExpiration = TimeSpan.FromMinutes(15);
+
     [HttpGet("{provider}")]
-    public ActionResult SignIn([FromRoute] string provider, [FromQuery] string? returnUrl = "/")
+    public async Task<ActionResult> SignIn([FromRoute] string provider, [FromQuery] string? returnUrl = "/")
     {
         try
         {
@@ -29,10 +34,11 @@ public class OidcController(
                 var state = Guid.NewGuid().ToString();
                 var nonce = Guid.NewGuid().ToString();
 
-                // Store user's ID, provider, and nonce in session. The callback will use this.
-                HttpContext.Session.SetString($"oidc_state_{state}", $"{currentUser.Id}|{provider}|{nonce}");
+                // Store user's ID, provider, and nonce in cache. The callback will use this.
+                var stateValue = $"{currentUser.Id}|{provider}|{nonce}";
+                await cache.SetAsync($"{StateCachePrefix}{state}", stateValue, StateExpiration);
 
-                // The state parameter sent to the provider is the GUID key for the session state.
+                // The state parameter sent to the provider is the GUID key for the cache.
                 var authUrl = oidcService.GetAuthorizationUrl(state, nonce);
                 return Redirect(authUrl);
             }
@@ -88,7 +94,7 @@ public class OidcController(
             );
 
             // Generate token using existing auth service
-            var token = authService.CreateToken(session);
+            var token = auth.CreateToken(session);
 
             return Ok(new AuthController.TokenExchangeResponse { Token = token });
         }
@@ -156,7 +162,7 @@ public class OidcController(
                 Meta = userInfo.ToMetadata()
             };
 
-            db.AccountConnections.Add(connection);
+            await db.AccountConnections.AddAsync(connection);
             await db.SaveChangesAsync();
 
             return existingAccount;
diff --git a/DysonNetwork.Sphere/wwwroot/css/styles.css b/DysonNetwork.Sphere/wwwroot/css/styles.css
index ef55696..7b65a38 100644
--- a/DysonNetwork.Sphere/wwwroot/css/styles.css
+++ b/DysonNetwork.Sphere/wwwroot/css/styles.css
@@ -1,4 +1,4 @@
-/*! tailwindcss v4.1.7 | MIT License | https://tailwindcss.com */
+/*! tailwindcss v4.1.10 | MIT License | https://tailwindcss.com */
 @layer properties;
 @layer theme, base, components, utilities;
 @layer theme {
@@ -311,6 +311,9 @@
   .max-w-lg {
     max-width: var(--container-lg);
   }
+  .border-collapse {
+    border-collapse: collapse;
+  }
   .transform {
     transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
   }
@@ -332,9 +335,23 @@
   .rounded-lg {
     border-radius: var(--radius-lg);
   }
+  .border {
+    border-style: var(--tw-border-style);
+    border-width: 1px;
+  }
+  .border-2 {
+    border-style: var(--tw-border-style);
+    border-width: 2px;
+  }
+  .border-gray-300 {
+    border-color: var(--color-gray-300);
+  }
   .bg-blue-500 {
     background-color: var(--color-blue-500);
   }
+  .bg-gray-100 {
+    background-color: var(--color-gray-100);
+  }
   .bg-green-100 {
     background-color: var(--color-green-100);
   }
@@ -430,6 +447,12 @@
   .text-yellow-800 {
     color: var(--color-yellow-800);
   }
+  .lowercase {
+    text-transform: lowercase;
+  }
+  .underline {
+    text-decoration-line: underline;
+  }
   .opacity-80 {
     opacity: 80%;
   }
@@ -504,6 +527,11 @@
       line-height: var(--tw-leading, var(--text-6xl--line-height));
     }
   }
+  .dark\:border-gray-600 {
+    @media (prefers-color-scheme: dark) {
+      border-color: var(--color-gray-600);
+    }
+  }
   .dark\:bg-gray-800 {
     @media (prefers-color-scheme: dark) {
       background-color: var(--color-gray-800);
@@ -800,6 +828,11 @@
   syntax: "*";
   inherits: false;
 }
+@property --tw-border-style {
+  syntax: "*";
+  inherits: false;
+  initial-value: solid;
+}
 @property --tw-leading {
   syntax: "*";
   inherits: false;
@@ -953,6 +986,7 @@
       --tw-rotate-z: initial;
       --tw-skew-x: initial;
       --tw-skew-y: initial;
+      --tw-border-style: solid;
       --tw-leading: initial;
       --tw-font-weight: initial;
       --tw-tracking: initial;