Index: binary-improvements2/WebServer/src/UrlHandlers/SessionHandler.cs
===================================================================
--- binary-improvements2/WebServer/src/UrlHandlers/SessionHandler.cs	(revision 399)
+++ binary-improvements2/WebServer/src/UrlHandlers/SessionHandler.cs	(revision 402)
@@ -1,4 +1,8 @@
 using System;
+using System.Collections.Generic;
+using System.IO;
 using System.Net;
+using Platform.Steam;
+using Utf8Json;
 
 namespace Webserver.UrlHandlers {
@@ -6,6 +10,8 @@
 		private const string pageBasePath = "/app";
 		private const string pageErrorPath = "/app/error/";
+		
 		private const string steamOpenIdVerifyUrl = "verifysteamopenid";
 		private const string steamLoginUrl = "loginsteam";
+		private const string userPassLoginUrl = "login";
 
 		private readonly ConnectionHandler connectionHandler;
@@ -37,11 +43,83 @@
 				return;
 			}
+			
+			if (subpath.StartsWith (userPassLoginUrl)) {
+				HandleUserPassLogin (_context);
+				return;
+			}
 
 			_context.Response.Redirect (pageErrorPath + "InvalidSessionsCommand");
 		}
 
+		private void HandleUserPassLogin (RequestContext _context) {
+			if (!_context.Request.HasEntityBody) {
+				_context.Response.Redirect (pageErrorPath + "NoLoginData");
+				return;
+			}
+
+			Stream requestInputStream = _context.Request.InputStream;
+
+			byte[] jsonInputData = new byte[_context.Request.ContentLength64];
+			requestInputStream.Read (jsonInputData, 0, (int)_context.Request.ContentLength64);
+
+			IDictionary<string, object> inputJson;
+			try {
+				inputJson = JsonSerializer.Deserialize<IDictionary<string, object>> (jsonInputData);
+			} catch (Exception e) {
+				Log.Error ("Error deserializing JSON from user/password login:");
+				Log.Exception (e);
+				_context.Response.Redirect (pageErrorPath + "InvalidLoginJson");
+				return;
+			}
+
+			if (!inputJson.TryGetValue ("username", out object fieldNode) || fieldNode is not string username) {
+				_context.Response.Redirect (pageErrorPath + "InvalidLoginJson");
+				return;
+			}
+
+			if (!inputJson.TryGetValue ("password", out fieldNode) || fieldNode is not string password) {
+				_context.Response.Redirect (pageErrorPath + "InvalidLoginJson");
+				return;
+			}
+
+			// TODO: Apply login
+
+			string remoteEndpointString = _context.Request.RemoteEndPoint!.ToString ();
+
+			if (username != "test" || password != "123") {
+				// TODO: failed login
+				Log.Out ($"[Web] User/pass login failed from {remoteEndpointString}");
+				_context.Response.Redirect (pageErrorPath + "UserPassInvalid");
+				return;
+			}
+			
+			try {
+				// TODO: Match username/password to UserIdentifierAbs / serveradmins.xml
+				
+				WebConnection con = connectionHandler.LogIn (new UserIdentifierSteam (76561198066968172ul), _context.Request.RemoteEndPoint.Address);
+				int level = GameManager.Instance.adminTools.GetUserPermissionLevel (con.UserId);
+				Log.Out ($"[Web] User/pass login from {remoteEndpointString} with ID {con.UserId}, permission level {level}");
+
+				Cookie cookie = new Cookie ("sid", con.SessionID, "/") {
+					Expired = false,
+					Expires = DateTime.MinValue,
+					HttpOnly = true,
+					Secure = false
+				};
+				_context.Response.AppendCookie (cookie);
+				_context.Response.Redirect (pageBasePath);
+
+				return;
+			} catch (Exception e) {
+				Log.Error ("[Web] Error during user/pass login:");
+				Log.Exception (e);
+			}
+
+			_context.Response.Redirect (pageErrorPath + "UserPassLoginFailed");
+		}
+
 		private void HandleSteamLogin (RequestContext _context) {
-			string host = (WebUtils.IsSslRedirected (_context.Request) ? "https://" : "http://") + _context.Request.UserHostName;
-			string url = OpenID.GetOpenIdLoginUrl (host, host + urlBasePath + steamOpenIdVerifyUrl);
+			string host = $"{(WebUtils.IsSslRedirected (_context.Request) ? "https://" : "http://")}{_context.Request.UserHostName}";
+			string url = OpenID.GetOpenIdLoginUrl (host, $"{host}{urlBasePath}{steamOpenIdVerifyUrl}");
 			_context.Response.Redirect (url);
 		}
@@ -68,8 +146,7 @@
 				ulong id = OpenID.Validate (_context.Request);
 				if (id > 0) {
-					WebConnection con = connectionHandler.LogIn (id, _context.Request.RemoteEndPoint.Address);
+					WebConnection con = connectionHandler.LogIn (new UserIdentifierSteam (id), _context.Request.RemoteEndPoint.Address);
 					int level = GameManager.Instance.adminTools.GetUserPermissionLevel (con.UserId);
-					Log.Out ("[Web] Steam OpenID login from {0} with ID {1}, permission level {2}",
-						remoteEndpointString, con.UserId, level);
+					Log.Out ($"[Web] Steam OpenID login from {remoteEndpointString} with ID {con.UserId}, permission level {level}");
 
 					Cookie cookie = new Cookie ("sid", con.SessionID, "/") {
