security patches & rate limits
This commit is contained in:
parent
dc893baf94
commit
4e08d1a2df
4 changed files with 106 additions and 42 deletions
|
|
@ -68,7 +68,7 @@ public class Requests
|
||||||
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
||||||
string password = await Utils.GetPassword(id);
|
string password = await Utils.GetPassword(id);
|
||||||
secret = await Utils.NonceDecryptBody(id, password, secret);
|
secret = await Utils.NonceDecryptBody(id, password, secret);
|
||||||
string auth = await Utils.Auth(id, password, secret);
|
string auth = await Utils.Auth(id, password, secret, context.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||||
if (auth != Utils.LOGIN_SUCCESS)
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
{
|
{
|
||||||
await context.Response.WriteAsync(auth);
|
await context.Response.WriteAsync(auth);
|
||||||
|
|
@ -103,9 +103,10 @@ public class Requests
|
||||||
{
|
{
|
||||||
case "init":
|
case "init":
|
||||||
{
|
{
|
||||||
//KEM keygen jest kosztowny - limit per IP
|
//check tylko: kwota jest zapisywana w finish po faktycznym utworzeniu konta,
|
||||||
|
//więc nieudane podejścia jej nie zjadają
|
||||||
string initIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
string initIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
||||||
if (RateLimited(AccountInitRateLimits, initIp, ACCOUNT_INIT_RATE_LIMIT))
|
if (IsRateLimitReached(AccountInitRateLimits, initIp, ACCOUNT_INIT_RATE_LIMIT))
|
||||||
{
|
{
|
||||||
await context.Response.WriteAsync("error:rate.limit");
|
await context.Response.WriteAsync("error:rate.limit");
|
||||||
return;
|
return;
|
||||||
|
|
@ -125,7 +126,12 @@ public class Requests
|
||||||
KeyExchangePayload payload = new KeyExchangePayload();
|
KeyExchangePayload payload = new KeyExchangePayload();
|
||||||
payload.pubX25519 = Convert.ToBase64String(serverInfo.pubX25519);
|
payload.pubX25519 = Convert.ToBase64String(serverInfo.pubX25519);
|
||||||
payload.pubMlKem = Convert.ToBase64String(serverInfo.pubMlKem);
|
payload.pubMlKem = Convert.ToBase64String(serverInfo.pubMlKem);
|
||||||
payload.idKey = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + "\n";
|
//losowy klucz: bez przejścia przez init nie da się odgadnąć idKey,
|
||||||
|
//więc register bez ważnego init robi tylko tani lookup
|
||||||
|
do
|
||||||
|
{
|
||||||
|
payload.idKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||||
|
} while (createHolder.ContainsKey(payload.idKey));
|
||||||
|
|
||||||
CreateHolder dataHolder = new();
|
CreateHolder dataHolder = new();
|
||||||
dataHolder.date = DateTimeOffset.UtcNow;
|
dataHolder.date = DateTimeOffset.UtcNow;
|
||||||
|
|
@ -134,11 +140,6 @@ public class Requests
|
||||||
dataHolder.KdfSalt = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
|
dataHolder.KdfSalt = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
|
||||||
payload.kdfSalt = dataHolder.KdfSalt;
|
payload.kdfSalt = dataHolder.KdfSalt;
|
||||||
|
|
||||||
while (createHolder.ContainsKey(payload.idKey))
|
|
||||||
{
|
|
||||||
payload.idKey += Random.Shared.Next(0, 10).ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
createHolder.TryAdd(payload.idKey, dataHolder);
|
createHolder.TryAdd(payload.idKey, dataHolder);
|
||||||
|
|
||||||
var serializedPayload = JsonSerializer.Serialize(
|
var serializedPayload = JsonSerializer.Serialize(
|
||||||
|
|
@ -163,6 +164,14 @@ public class Requests
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//register jest jednorazowy w tej sesji: powtórka (albo równoległe żądanie)
|
||||||
|
//z tym samym idKey nie może drugi raz liczyć KEM
|
||||||
|
if (Interlocked.CompareExchange(ref entry.registerDone, 1, 0) != 0)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("error:account.creation.request.expired");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
entry.date = DateTimeOffset.UtcNow;
|
entry.date = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
byte[] pubX25519Client = Convert.FromBase64String(serializedBody.pubX25519);
|
byte[] pubX25519Client = Convert.FromBase64String(serializedBody.pubX25519);
|
||||||
|
|
@ -315,6 +324,9 @@ public class Requests
|
||||||
userLock.Release();
|
userLock.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//kwota liczona dopiero po faktycznym utworzeniu konta
|
||||||
|
RecordRateLimitHit(AccountInitRateLimits,
|
||||||
|
context.Connection.RemoteIpAddress?.ToString() ?? "", 3600);
|
||||||
await context.Response.WriteAsync("success:account.created");
|
await context.Response.WriteAsync("success:account.created");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -343,18 +355,11 @@ public class Requests
|
||||||
}
|
}
|
||||||
|
|
||||||
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
||||||
|
|
||||||
//rate limit prob logowania (konto+IP)
|
|
||||||
string clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
string clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
||||||
if (RateLimited(AuthRateLimits, $"{id}:{clientIp}", AUTH_RATE_LIMIT))
|
|
||||||
{
|
|
||||||
await context.Response.WriteAsync("error:rate.limit");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string password = await Utils.GetPassword(id);
|
string password = await Utils.GetPassword(id);
|
||||||
secret = await Utils.NonceDecryptBody(id, password, secret);
|
secret = await Utils.NonceDecryptBody(id, password, secret);
|
||||||
string auth = await Utils.Auth(id, password, secret);
|
string auth = await Utils.Auth(id, password, secret, clientIp);
|
||||||
|
|
||||||
await context.Response.WriteAsync(auth);
|
await context.Response.WriteAsync(auth);
|
||||||
return;
|
return;
|
||||||
|
|
@ -511,7 +516,7 @@ public class Requests
|
||||||
string bodyDecrypted = await Utils.NonceDecryptBody(id, password, body, false);
|
string bodyDecrypted = await Utils.NonceDecryptBody(id, password, body, false);
|
||||||
secret = await Utils.NonceDecryptBody(id, password, secret);
|
secret = await Utils.NonceDecryptBody(id, password, secret);
|
||||||
|
|
||||||
string auth = await Utils.Auth(id, password, secret);
|
string auth = await Utils.Auth(id, password, secret, context.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||||
|
|
||||||
if (auth != Utils.LOGIN_SUCCESS)
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
{
|
{
|
||||||
|
|
@ -598,7 +603,7 @@ public class Requests
|
||||||
body = await Utils.NonceDecryptBody(id, password, body, false);
|
body = await Utils.NonceDecryptBody(id, password, body, false);
|
||||||
secret = await Utils.NonceDecryptBody(id, password, secret);
|
secret = await Utils.NonceDecryptBody(id, password, secret);
|
||||||
|
|
||||||
string auth = await Utils.Auth(id, secret, password);
|
string auth = await Utils.Auth(id, secret, password, context.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||||
if (auth != Utils.LOGIN_SUCCESS)
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
{
|
{
|
||||||
await context.Response.WriteAsync(auth);
|
await context.Response.WriteAsync(auth);
|
||||||
|
|
@ -726,7 +731,7 @@ public class Requests
|
||||||
AppJsonSerializerContext.Default.Universal2String
|
AppJsonSerializerContext.Default.Universal2String
|
||||||
);
|
);
|
||||||
|
|
||||||
string auth = await Utils.Auth(id, secret, password);
|
string auth = await Utils.Auth(id, secret, password, context.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||||
|
|
||||||
if (auth != Utils.LOGIN_SUCCESS)
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
{
|
{
|
||||||
|
|
@ -1216,7 +1221,7 @@ public class Requests
|
||||||
string target = await Utils.NonceDecryptBody(id, password, targetHeader, false);
|
string target = await Utils.NonceDecryptBody(id, password, targetHeader, false);
|
||||||
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
||||||
|
|
||||||
string auth = await Utils.Auth(id, secretStr, password);
|
string auth = await Utils.Auth(id, secretStr, password, context.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||||
|
|
||||||
if (auth != Utils.LOGIN_SUCCESS)
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
{
|
{
|
||||||
|
|
@ -1276,7 +1281,7 @@ public class Requests
|
||||||
//S9: jednorazowa akcja zapisu - konsumuj nonce (delEntry:false pozwalał
|
//S9: jednorazowa akcja zapisu - konsumuj nonce (delEntry:false pozwalał
|
||||||
//na nieograniczony replay przechwyconej pary secret+body)
|
//na nieograniczony replay przechwyconej pary secret+body)
|
||||||
string secretStr = await Utils.NonceDecryptBody(id, password, secret, true);
|
string secretStr = await Utils.NonceDecryptBody(id, password, secret, true);
|
||||||
string auth = await Utils.Auth(id, secretStr, password);
|
string auth = await Utils.Auth(id, secretStr, password, context.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||||
|
|
||||||
if (auth != Utils.LOGIN_SUCCESS)
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
{
|
{
|
||||||
|
|
@ -1340,7 +1345,7 @@ public class Requests
|
||||||
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
||||||
string password = await Utils.GetPassword(id);
|
string password = await Utils.GetPassword(id);
|
||||||
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
||||||
string auth = await Utils.Auth(id, secretStr, password);
|
string auth = await Utils.Auth(id, secretStr, password, context.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||||
|
|
||||||
if (auth != Utils.LOGIN_SUCCESS) return; // Don't leak info
|
if (auth != Utils.LOGIN_SUCCESS) return; // Don't leak info
|
||||||
|
|
||||||
|
|
@ -1381,7 +1386,7 @@ public class Requests
|
||||||
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
||||||
string password = await Utils.GetPassword(id);
|
string password = await Utils.GetPassword(id);
|
||||||
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
||||||
string auth = await Utils.Auth(id, secretStr, password);
|
string auth = await Utils.Auth(id, secretStr, password, context.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||||
|
|
||||||
if (auth != Utils.LOGIN_SUCCESS) return;
|
if (auth != Utils.LOGIN_SUCCESS) return;
|
||||||
|
|
||||||
|
|
@ -1414,6 +1419,7 @@ public class Requests
|
||||||
public class CreateHolder
|
public class CreateHolder
|
||||||
{
|
{
|
||||||
public DateTimeOffset date;
|
public DateTimeOffset date;
|
||||||
|
public int registerDone;
|
||||||
public string name;
|
public string name;
|
||||||
public string pass;
|
public string pass;
|
||||||
public string captcha;
|
public string captcha;
|
||||||
|
|
|
||||||
|
|
@ -151,17 +151,23 @@ public class Utils
|
||||||
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<string> Auth(string id, string password, string password2)
|
public static async Task<string> Auth(string id, string password, string password2, string clientIp = "")
|
||||||
{
|
{
|
||||||
|
if (IsAuthRateLimited(id, clientIp))
|
||||||
|
{
|
||||||
|
return "error:invalid.password";
|
||||||
|
}
|
||||||
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}"))
|
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}"))
|
||||||
{
|
{
|
||||||
return "error:invalid.username.or.password";
|
return "error:invalid.username.or.password";
|
||||||
}
|
}
|
||||||
if (password != password2)
|
if (password != password2)
|
||||||
{
|
{
|
||||||
|
RecordAuthFailure(id, clientIp);
|
||||||
return "error:invalid.password";
|
return "error:invalid.password";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RecordAuthSuccess(id, clientIp);
|
||||||
await UpdateLastLogin(id);
|
await UpdateLastLogin(id);
|
||||||
return LOGIN_SUCCESS;
|
return LOGIN_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ public class Program
|
||||||
{
|
{
|
||||||
if (entry.Value.resetAt <= nowSec) VoiceAuthLimits.TryRemove(entry.Key, out _);
|
if (entry.Value.resetAt <= nowSec) VoiceAuthLimits.TryRemove(entry.Key, out _);
|
||||||
}
|
}
|
||||||
//prune pozostałych rate limitów (okna 60s)
|
//prune pozostałych rate limitów (okna 60s / 1h)
|
||||||
PruneRateLimits(AuthRateLimits, nowSec);
|
PruneRateLimits(AuthRateLimits, nowSec);
|
||||||
PruneRateLimits(NonceRateLimits, nowSec);
|
PruneRateLimits(NonceRateLimits, nowSec);
|
||||||
PruneRateLimits(KdfSaltRateLimits, nowSec);
|
PruneRateLimits(KdfSaltRateLimits, nowSec);
|
||||||
|
|
|
||||||
|
|
@ -30,26 +30,85 @@ public class Utils
|
||||||
// Fixed 60s window: (request count, window reset unix time).
|
// Fixed 60s window: (request count, window reset unix time).
|
||||||
public static ConcurrentDictionary<string, (int count, long resetAt)> VoiceAuthLimits = new();
|
public static ConcurrentDictionary<string, (int count, long resetAt)> VoiceAuthLimits = new();
|
||||||
|
|
||||||
// Rate limity per-minute (okno 60s). Wszystkie konfigurowalne przez .env.
|
// Rate limity per IP/konto (okno stałe, domyślnie 60s). Wszystkie konfigurowalne przez .env.
|
||||||
public static ConcurrentDictionary<string, (int count, long resetAt)> AuthRateLimits = new();
|
public static ConcurrentDictionary<string, (int count, long resetAt)> AuthRateLimits = new();
|
||||||
public static ConcurrentDictionary<string, (int count, long resetAt)> NonceRateLimits = new();
|
public static ConcurrentDictionary<string, (int count, long resetAt)> NonceRateLimits = new();
|
||||||
public static ConcurrentDictionary<string, (int count, long resetAt)> KdfSaltRateLimits = new();
|
public static ConcurrentDictionary<string, (int count, long resetAt)> KdfSaltRateLimits = new();
|
||||||
public static ConcurrentDictionary<string, (int count, long resetAt)> AccountInitRateLimits = new();
|
public static ConcurrentDictionary<string, (int count, long resetAt)> AccountInitRateLimits = new();
|
||||||
|
|
||||||
public static int AUTH_RATE_LIMIT = 10; // próby logowania /auth + /ws na konto+IP
|
public static int AUTH_RATE_LIMIT = 10; // failed password verifications per account+IP per minute
|
||||||
public static int NONCE_RATE_LIMIT = 240; // pobrania nonce per konto
|
public static int NONCE_RATE_LIMIT = 240; // pobrania nonce per konto per minute
|
||||||
public static int KDFSALT_RATE_LIMIT = 60; // pobrania soli per IP
|
public static int KDFSALT_RATE_LIMIT = 60; // pobrania soli per IP per minute
|
||||||
public static int ACCOUNT_INIT_RATE_LIMIT = 10; // inicjalizacje rejestracji (KEM keygen) per IP
|
public static int ACCOUNT_INIT_RATE_LIMIT = 4; // utworzone konta per IP per hour (check w init, zapis w finish)
|
||||||
|
|
||||||
// Zwraca true jeśli przekroczono limit. Okno stałe 60s.
|
// Zwraca true jeśli przekroczono limit. Okno stałe, domyślnie 60s.
|
||||||
public static bool RateLimited(ConcurrentDictionary<string, (int count, long resetAt)> store, string key, int limit)
|
public static bool RateLimited(ConcurrentDictionary<string, (int count, long resetAt)> store, string key, int limit, int windowSeconds = 60)
|
||||||
{
|
{
|
||||||
if (limit <= 0) return false; // <=0 wyłącza limit
|
if (limit <= 0) return false; // <=0 wyłącza limit
|
||||||
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
var window = store.AddOrUpdate(key,
|
var window = store.AddOrUpdate(key,
|
||||||
|
(1, nowSec + windowSeconds),
|
||||||
|
(_, old) => old.resetAt <= nowSec ? (1, nowSec + windowSeconds) : (old.count + 1, old.resetAt));
|
||||||
|
return window.count > limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Sprawdzenie bez inkrementacji: true jeśli klucz osiągnął już limit w bieżącym oknie
|
||||||
|
public static bool IsRateLimitReached(ConcurrentDictionary<string, (int count, long resetAt)> store, string key, int limit)
|
||||||
|
{
|
||||||
|
if (limit <= 0) return false;
|
||||||
|
if (!store.TryGetValue(key, out var entry)) return false;
|
||||||
|
|
||||||
|
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
if (entry.resetAt <= nowSec)
|
||||||
|
{
|
||||||
|
store.TryRemove(key, out _);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entry.count >= limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Zapis jednego zdarzenia; okno startuje przy pierwszym zapisie
|
||||||
|
public static void RecordRateLimitHit(ConcurrentDictionary<string, (int count, long resetAt)> store, string key, int windowSeconds = 60)
|
||||||
|
{
|
||||||
|
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
store.AddOrUpdate(key,
|
||||||
|
(1, nowSec + windowSeconds),
|
||||||
|
(_, old) => old.resetAt <= nowSec ? (1, nowSec + windowSeconds) : (old.count + 1, old.resetAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed password verifications only, keyed "{id}:{ip}". A success clears the
|
||||||
|
// counter unless the limit is already reached; then the 60s window must expire.
|
||||||
|
public static bool IsAuthRateLimited(string id, string clientIp)
|
||||||
|
{
|
||||||
|
if (AUTH_RATE_LIMIT <= 0) return false;
|
||||||
|
string key = $"{id}:{clientIp}";
|
||||||
|
if (!AuthRateLimits.TryGetValue(key, out var entry)) return false;
|
||||||
|
|
||||||
|
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
if (entry.resetAt <= nowSec)
|
||||||
|
{
|
||||||
|
AuthRateLimits.TryRemove(key, out _);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entry.count >= AUTH_RATE_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RecordAuthFailure(string id, string clientIp)
|
||||||
|
{
|
||||||
|
if (AUTH_RATE_LIMIT <= 0) return;
|
||||||
|
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
AuthRateLimits.AddOrUpdate($"{id}:{clientIp}",
|
||||||
(1, nowSec + 60),
|
(1, nowSec + 60),
|
||||||
(_, old) => old.resetAt <= nowSec ? (1, nowSec + 60) : (old.count + 1, old.resetAt));
|
(_, old) => old.resetAt <= nowSec ? (1, nowSec + 60) : (old.count + 1, old.resetAt));
|
||||||
return window.count > limit;
|
}
|
||||||
|
|
||||||
|
public static void RecordAuthSuccess(string id, string clientIp)
|
||||||
|
{
|
||||||
|
string key = $"{id}:{clientIp}";
|
||||||
|
if (AuthRateLimits.TryGetValue(key, out var entry) && entry.count < AUTH_RATE_LIMIT)
|
||||||
|
{
|
||||||
|
AuthRateLimits.TryRemove(key, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string DATA_DIR = "./data";
|
public static string DATA_DIR = "./data";
|
||||||
|
|
@ -188,19 +247,12 @@ public class Utils
|
||||||
}
|
}
|
||||||
|
|
||||||
string id = Account.Utils.GetValidIdOrZero(authParams.string1);
|
string id = Account.Utils.GetValidIdOrZero(authParams.string1);
|
||||||
|
|
||||||
//rate limit prób autoryzacji WS (konto+IP) - wspólny zasobnik z /auth
|
|
||||||
string clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
string clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "";
|
||||||
if (RateLimited(AuthRateLimits, $"{id}:{clientIp}", AUTH_RATE_LIMIT))
|
|
||||||
{
|
|
||||||
await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.PolicyViolation, "Rate limited", CancellationToken.None);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string password = await Account.Utils.GetPassword(id);
|
string password = await Account.Utils.GetPassword(id);
|
||||||
string secret = await Account.Utils.NonceDecryptBody(id, password, authParams.string2);
|
string secret = await Account.Utils.NonceDecryptBody(id, password, authParams.string2);
|
||||||
|
|
||||||
string auth = await Account.Utils.Auth(id, password, secret);
|
string auth = await Account.Utils.Auth(id, password, secret, clientIp);
|
||||||
|
|
||||||
if (auth != Account.Utils.LOGIN_SUCCESS)
|
if (auth != Account.Utils.LOGIN_SUCCESS)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue