23 01 25
Сделал авторификацию новую через sessionKey (Но пока тесты сессии падают)
This commit is contained in:
parent
580695b486
commit
e84c63c3d1
@ -0,0 +1,2 @@
|
|||||||
|
НАПИШИ ВНАЧАЛЕ ФОРМАТ ОБЩЕГО ЗАГЛАВИЯ.
|
||||||
|
А ПОТОМ ФОРМАТ ПО КАЖДОМУ ТИПУ (И В НЁМ СУБТИПУ БЛОКОВ) ДЛЯ ЧЕГО НАДО, ЧТО ХРАНИТЬСЯ, КАКИЕ ПРАВИЛА И ОСОБЕННОСТИ ЗАПОЛНЕНИЯ
|
||||||
@ -18,9 +18,9 @@ import java.sql.Statement;
|
|||||||
* - создаём ТОЛЬКО таблицы/индексы
|
* - создаём ТОЛЬКО таблицы/индексы
|
||||||
* - в конце вызываем DatabaseTriggersInstaller.createAllTriggers(st)
|
* - в конце вызываем DatabaseTriggersInstaller.createAllTriggers(st)
|
||||||
*
|
*
|
||||||
* Зачем так:
|
* v2 (sessions):
|
||||||
* - триггеры часто ломают совместимость с внешними SQLite-просмотрщиками/сборками
|
* - active_sessions.session_pwd удалён
|
||||||
* - проще поддерживать/мигрировать
|
* - active_sessions.session_key хранит публичный ключ сессии (sessionPubKeyB64)
|
||||||
*/
|
*/
|
||||||
public final class DatabaseInitializer {
|
public final class DatabaseInitializer {
|
||||||
|
|
||||||
@ -28,25 +28,16 @@ public final class DatabaseInitializer {
|
|||||||
|
|
||||||
/* ===================== TEXT (msg_type=1) ===================== */
|
/* ===================== TEXT (msg_type=1) ===================== */
|
||||||
|
|
||||||
/** Новое сообщение (начало ветки). */
|
|
||||||
public static final short TEXT_NEW = 1;
|
public static final short TEXT_NEW = 1;
|
||||||
|
|
||||||
/** Ответ на сообщение (reply). */
|
|
||||||
public static final short TEXT_REPLY = 2;
|
public static final short TEXT_REPLY = 2;
|
||||||
|
|
||||||
/** Репост (repost). */
|
|
||||||
public static final short TEXT_REPOST = 3;
|
public static final short TEXT_REPOST = 3;
|
||||||
|
|
||||||
/** Редактирование (edit). ВАЖНО: серверное значение = 10. */
|
|
||||||
public static final short TEXT_EDIT = 10;
|
public static final short TEXT_EDIT = 10;
|
||||||
|
|
||||||
/* ===================== REACTION (msg_type=2) ===================== */
|
/* ===================== REACTION (msg_type=2) ===================== */
|
||||||
|
|
||||||
/** Лайк (LIKE). */
|
|
||||||
public static final short REACTION_LIKE = 1;
|
public static final short REACTION_LIKE = 1;
|
||||||
|
|
||||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||||
// FRIEND=10/11, CONTACT=20/21, FOLLOW=30/31
|
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_FRIEND = 10;
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNFRIEND = 11;
|
||||||
|
|
||||||
@ -123,12 +114,12 @@ public final class DatabaseInitializer {
|
|||||||
ON solana_users (login);
|
ON solana_users (login);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// 2. active_sessions
|
// 2. active_sessions (v2)
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS active_sessions (
|
CREATE TABLE IF NOT EXISTS active_sessions (
|
||||||
session_id TEXT NOT NULL PRIMARY KEY,
|
session_id TEXT NOT NULL PRIMARY KEY,
|
||||||
login TEXT NOT NULL,
|
login TEXT NOT NULL,
|
||||||
session_pwd TEXT NOT NULL,
|
session_key TEXT NOT NULL,
|
||||||
storage_pwd TEXT NOT NULL,
|
storage_pwd TEXT NOT NULL,
|
||||||
session_created_at_ms INTEGER NOT NULL,
|
session_created_at_ms INTEGER NOT NULL,
|
||||||
last_authirificated_at_ms INTEGER NOT NULL,
|
last_authirificated_at_ms INTEGER NOT NULL,
|
||||||
@ -325,7 +316,6 @@ public final class DatabaseInitializer {
|
|||||||
ON message_stats (to_login);
|
ON message_stats (to_login);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// ВАЖНО: триггеры ставим отдельно
|
|
||||||
DatabaseTriggersInstaller.createAllTriggers(st);
|
DatabaseTriggersInstaller.createAllTriggers(st);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,13 +32,12 @@ public final class ActiveSessionsDAO {
|
|||||||
|
|
||||||
// -------------------- INSERT --------------------
|
// -------------------- INSERT --------------------
|
||||||
|
|
||||||
/** Вставка с внешним соединением. Соединение НЕ закрывает. */
|
|
||||||
public void insert(Connection c, ActiveSessionEntry session) throws SQLException {
|
public void insert(Connection c, ActiveSessionEntry session) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO active_sessions (
|
INSERT INTO active_sessions (
|
||||||
session_id,
|
session_id,
|
||||||
login,
|
login,
|
||||||
session_pwd,
|
session_key,
|
||||||
storage_pwd,
|
storage_pwd,
|
||||||
session_created_at_ms,
|
session_created_at_ms,
|
||||||
last_authirificated_at_ms,
|
last_authirificated_at_ms,
|
||||||
@ -55,7 +54,7 @@ public final class ActiveSessionsDAO {
|
|||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, session.getSessionId());
|
ps.setString(1, session.getSessionId());
|
||||||
ps.setString(2, session.getLogin());
|
ps.setString(2, session.getLogin());
|
||||||
ps.setString(3, session.getSessionPwd());
|
ps.setString(3, session.getSessionKey());
|
||||||
ps.setString(4, session.getStoragePwd());
|
ps.setString(4, session.getStoragePwd());
|
||||||
ps.setLong(5, session.getSessionCreatedAtMs());
|
ps.setLong(5, session.getSessionCreatedAtMs());
|
||||||
ps.setLong(6, session.getLastAuthirificatedAtMs());
|
ps.setLong(6, session.getLastAuthirificatedAtMs());
|
||||||
@ -70,7 +69,6 @@ public final class ActiveSessionsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Вставка без внешнего соединения. Сам открывает/закрывает. */
|
|
||||||
public void insert(ActiveSessionEntry session) throws SQLException {
|
public void insert(ActiveSessionEntry session) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
insert(c, session);
|
insert(c, session);
|
||||||
@ -79,13 +77,12 @@ public final class ActiveSessionsDAO {
|
|||||||
|
|
||||||
// -------------------- SELECT --------------------
|
// -------------------- SELECT --------------------
|
||||||
|
|
||||||
/** Получить по sessionId с внешним соединением. Соединение НЕ закрывает. */
|
|
||||||
public ActiveSessionEntry getBySessionId(Connection c, String sessionId) throws SQLException {
|
public ActiveSessionEntry getBySessionId(Connection c, String sessionId) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
session_id,
|
session_id,
|
||||||
login,
|
login,
|
||||||
session_pwd,
|
session_key,
|
||||||
storage_pwd,
|
storage_pwd,
|
||||||
session_created_at_ms,
|
session_created_at_ms,
|
||||||
last_authirificated_at_ms,
|
last_authirificated_at_ms,
|
||||||
@ -109,20 +106,18 @@ public final class ActiveSessionsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Получить по sessionId без внешнего соединения. Сам открывает/закрывает. */
|
|
||||||
public ActiveSessionEntry getBySessionId(String sessionId) throws SQLException {
|
public ActiveSessionEntry getBySessionId(String sessionId) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return getBySessionId(c, sessionId);
|
return getBySessionId(c, sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Получить список по login с внешним соединением. Соединение НЕ закрывает. */
|
|
||||||
public List<ActiveSessionEntry> getByLogin(Connection c, String login) throws SQLException {
|
public List<ActiveSessionEntry> getByLogin(Connection c, String login) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
session_id,
|
session_id,
|
||||||
login,
|
login,
|
||||||
session_pwd,
|
session_key,
|
||||||
storage_pwd,
|
storage_pwd,
|
||||||
session_created_at_ms,
|
session_created_at_ms,
|
||||||
last_authirificated_at_ms,
|
last_authirificated_at_ms,
|
||||||
@ -149,7 +144,6 @@ public final class ActiveSessionsDAO {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Получить список по login без внешнего соединения. Сам открывает/закрывает. */
|
|
||||||
public List<ActiveSessionEntry> getByLogin(String login) throws SQLException {
|
public List<ActiveSessionEntry> getByLogin(String login) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return getByLogin(c, login);
|
return getByLogin(c, login);
|
||||||
@ -158,7 +152,6 @@ public final class ActiveSessionsDAO {
|
|||||||
|
|
||||||
// -------------------- UPDATE --------------------
|
// -------------------- UPDATE --------------------
|
||||||
|
|
||||||
/** Обновить lastAuthirificatedAtMs с внешним соединением. Соединение НЕ закрывает. */
|
|
||||||
public void updateLastAuthirificatedAtMs(Connection c, String sessionId, long lastAuthMs) throws SQLException {
|
public void updateLastAuthirificatedAtMs(Connection c, String sessionId, long lastAuthMs) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
UPDATE active_sessions
|
UPDATE active_sessions
|
||||||
@ -173,14 +166,12 @@ public final class ActiveSessionsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обновить lastAuthirificatedAtMs без внешнего соединения. Сам открывает/закрывает. */
|
|
||||||
public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
|
public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
updateLastAuthirificatedAtMs(c, sessionId, lastAuthMs);
|
updateLastAuthirificatedAtMs(c, sessionId, lastAuthMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обновить данные refresh с внешним соединением. Соединение НЕ закрывает. */
|
|
||||||
public void updateOnRefresh(
|
public void updateOnRefresh(
|
||||||
Connection c,
|
Connection c,
|
||||||
String sessionId,
|
String sessionId,
|
||||||
@ -213,7 +204,6 @@ public final class ActiveSessionsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обновить данные refresh без внешнего соединения. Сам открывает/закрывает. */
|
|
||||||
public void updateOnRefresh(
|
public void updateOnRefresh(
|
||||||
String sessionId,
|
String sessionId,
|
||||||
long lastAuthMs,
|
long lastAuthMs,
|
||||||
@ -229,7 +219,6 @@ public final class ActiveSessionsDAO {
|
|||||||
|
|
||||||
// -------------------- DELETE --------------------
|
// -------------------- DELETE --------------------
|
||||||
|
|
||||||
/** Удалить по sessionId с внешним соединением. Соединение НЕ закрывает. */
|
|
||||||
public void deleteBySessionId(Connection c, String sessionId) throws SQLException {
|
public void deleteBySessionId(Connection c, String sessionId) throws SQLException {
|
||||||
String sql = "DELETE FROM active_sessions WHERE session_id = ?";
|
String sql = "DELETE FROM active_sessions WHERE session_id = ?";
|
||||||
|
|
||||||
@ -239,7 +228,6 @@ public final class ActiveSessionsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Удалить по sessionId без внешнего соединения. Сам открывает/закрывает. */
|
|
||||||
public void deleteBySessionId(String sessionId) throws SQLException {
|
public void deleteBySessionId(String sessionId) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
deleteBySessionId(c, sessionId);
|
deleteBySessionId(c, sessionId);
|
||||||
@ -251,7 +239,7 @@ public final class ActiveSessionsDAO {
|
|||||||
private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException {
|
private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
String sessionId = rs.getString("session_id");
|
String sessionId = rs.getString("session_id");
|
||||||
String login = rs.getString("login");
|
String login = rs.getString("login");
|
||||||
String sessionPwd = rs.getString("session_pwd");
|
String sessionKey = rs.getString("session_key");
|
||||||
String storagePwd = rs.getString("storage_pwd");
|
String storagePwd = rs.getString("storage_pwd");
|
||||||
long sessionCreatedAtMs = rs.getLong("session_created_at_ms");
|
long sessionCreatedAtMs = rs.getLong("session_created_at_ms");
|
||||||
long lastAuthirificatedAtMs = rs.getLong("last_authirificated_at_ms");
|
long lastAuthirificatedAtMs = rs.getLong("last_authirificated_at_ms");
|
||||||
@ -266,7 +254,7 @@ public final class ActiveSessionsDAO {
|
|||||||
return new ActiveSessionEntry(
|
return new ActiveSessionEntry(
|
||||||
sessionId,
|
sessionId,
|
||||||
login,
|
login,
|
||||||
sessionPwd,
|
sessionKey,
|
||||||
storagePwd,
|
storagePwd,
|
||||||
sessionCreatedAtMs,
|
sessionCreatedAtMs,
|
||||||
lastAuthirificatedAtMs,
|
lastAuthirificatedAtMs,
|
||||||
|
|||||||
@ -7,10 +7,14 @@ public class ActiveSessionEntry {
|
|||||||
|
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
private String login;
|
private String login;
|
||||||
private String sessionPwd;
|
|
||||||
|
/** session_key: публичный ключ сессии (base64 от 32 байт). */
|
||||||
|
private String sessionKey;
|
||||||
|
|
||||||
private String storagePwd;
|
private String storagePwd;
|
||||||
private long sessionCreatedAtMs;
|
private long sessionCreatedAtMs;
|
||||||
private long lastAuthirificatedAtMs;
|
private long lastAuthirificatedAtMs;
|
||||||
|
|
||||||
private String pushEndpoint;
|
private String pushEndpoint;
|
||||||
private String pushP256dhKey;
|
private String pushP256dhKey;
|
||||||
private String pushAuthKey;
|
private String pushAuthKey;
|
||||||
@ -20,12 +24,11 @@ public class ActiveSessionEntry {
|
|||||||
private String clientInfoFromRequest;
|
private String clientInfoFromRequest;
|
||||||
private String userLanguage;
|
private String userLanguage;
|
||||||
|
|
||||||
public ActiveSessionEntry() {
|
public ActiveSessionEntry() { }
|
||||||
}
|
|
||||||
|
|
||||||
public ActiveSessionEntry(String sessionId,
|
public ActiveSessionEntry(String sessionId,
|
||||||
String login,
|
String login,
|
||||||
String sessionPwd,
|
String sessionKey,
|
||||||
String storagePwd,
|
String storagePwd,
|
||||||
long sessionCreatedAtMs,
|
long sessionCreatedAtMs,
|
||||||
long lastAuthirificatedAtMs,
|
long lastAuthirificatedAtMs,
|
||||||
@ -38,7 +41,7 @@ public class ActiveSessionEntry {
|
|||||||
String userLanguage) {
|
String userLanguage) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.login = login;
|
this.login = login;
|
||||||
this.sessionPwd = sessionPwd;
|
this.sessionKey = sessionKey;
|
||||||
this.storagePwd = storagePwd;
|
this.storagePwd = storagePwd;
|
||||||
this.sessionCreatedAtMs = sessionCreatedAtMs;
|
this.sessionCreatedAtMs = sessionCreatedAtMs;
|
||||||
this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
|
this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
|
||||||
@ -57,8 +60,8 @@ public class ActiveSessionEntry {
|
|||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
public String getSessionPwd() { return sessionPwd; }
|
public String getSessionKey() { return sessionKey; }
|
||||||
public void setSessionPwd(String sessionPwd) { this.sessionPwd = sessionPwd; }
|
public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; }
|
||||||
|
|
||||||
public String getStoragePwd() { return storagePwd; }
|
public String getStoragePwd() { return storagePwd; }
|
||||||
public void setStoragePwd(String storagePwd) { this.storagePwd = storagePwd; }
|
public void setStoragePwd(String storagePwd) { this.storagePwd = storagePwd; }
|
||||||
|
|||||||
@ -7,12 +7,22 @@ import shine.db.entities.ActiveSessionEntry;
|
|||||||
/**
|
/**
|
||||||
* ConnectionContext — контекст состояния одного WebSocket-соединения.
|
* ConnectionContext — контекст состояния одного WebSocket-соединения.
|
||||||
* Живёт ровно столько же, сколько живёт подключение.
|
* Живёт ровно столько же, сколько живёт подключение.
|
||||||
|
*
|
||||||
|
* Важно (v2):
|
||||||
|
* - Авторизация всегда 2 шага:
|
||||||
|
* A) Создание новой сессии через deviceKey:
|
||||||
|
* AuthChallenge(login) -> ctx.authNonce
|
||||||
|
* CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
|
||||||
|
*
|
||||||
|
* B) Вход в существующую сессию через sessionKey:
|
||||||
|
* SessionChallenge(sessionId) -> ctx.sessionLoginNonce + ctx.sessionLoginSessionId + expiresAt
|
||||||
|
* SessionLogin(...) -> проверка подписи sessionKey по pubkey из БД -> ctx.AUTH_STATUS_USER
|
||||||
*/
|
*/
|
||||||
public class ConnectionContext {
|
public class ConnectionContext {
|
||||||
|
|
||||||
// Статусы аутентификации
|
// Статусы аутентификации
|
||||||
public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
|
public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
|
||||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // получен AuthChallenge
|
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
|
||||||
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
||||||
|
|
||||||
// Полный пользователь из БД (solana_users)
|
// Полный пользователь из БД (solana_users)
|
||||||
@ -23,20 +33,38 @@ public class ConnectionContext {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Идентификатор сессии — base64-строка от 32 байт.
|
* Идентификатор сессии — base64-строка от 32 байт.
|
||||||
|
* Заполняется после успешного входа (AUTH_STATUS_USER).
|
||||||
*/
|
*/
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
|
||||||
/**
|
|
||||||
* Секрет сессии (то, что хранится в active_sessions.session_pwd).
|
|
||||||
*/
|
|
||||||
private String sessionPwd;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
|
* Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
|
||||||
* используется на шаге 2 для проверки подписи.
|
* используется на шаге CreateAuthSession для проверки подписи deviceKey.
|
||||||
*/
|
*/
|
||||||
private String authNonce;
|
private String authNonce;
|
||||||
|
|
||||||
|
/* ===================== SessionLogin challenge (v2) ===================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Одноразовый nonce, выданный на шаге SessionChallenge(sessionId),
|
||||||
|
* используется на шаге SessionLogin для проверки подписи sessionKey.
|
||||||
|
*/
|
||||||
|
private String sessionLoginNonce;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sessionId, для которого был выдан sessionLoginNonce.
|
||||||
|
* Нужен, чтобы SessionLogin не мог "подставить" другой sessionId.
|
||||||
|
*/
|
||||||
|
private String sessionLoginSessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Время истечения sessionLoginNonce (мс с 1970-01-01).
|
||||||
|
* Если текущее время > expiresAt, то nonce считается недействительным.
|
||||||
|
*/
|
||||||
|
private long sessionLoginNonceExpiresAtMs;
|
||||||
|
|
||||||
|
/* ====================================================================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Текущий статус аутентификации.
|
* Текущий статус аутентификации.
|
||||||
* См. константы AUTH_STATUS_*
|
* См. константы AUTH_STATUS_*
|
||||||
@ -83,7 +111,7 @@ public class ConnectionContext {
|
|||||||
return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
|
return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- sessionId / sessionPwd ---
|
// --- sessionId ---
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
@ -93,14 +121,6 @@ public class ConnectionContext {
|
|||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSessionPwd() {
|
|
||||||
return sessionPwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionPwd(String sessionPwd) {
|
|
||||||
this.sessionPwd = sessionPwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- authNonce ---
|
// --- authNonce ---
|
||||||
|
|
||||||
public String getAuthNonce() {
|
public String getAuthNonce() {
|
||||||
@ -111,6 +131,32 @@ public class ConnectionContext {
|
|||||||
this.authNonce = authNonce;
|
this.authNonce = authNonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- sessionLoginNonce (v2) ---
|
||||||
|
|
||||||
|
public String getSessionLoginNonce() {
|
||||||
|
return sessionLoginNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionLoginNonce(String sessionLoginNonce) {
|
||||||
|
this.sessionLoginNonce = sessionLoginNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionLoginSessionId() {
|
||||||
|
return sessionLoginSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionLoginSessionId(String sessionLoginSessionId) {
|
||||||
|
this.sessionLoginSessionId = sessionLoginSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSessionLoginNonceExpiresAtMs() {
|
||||||
|
return sessionLoginNonceExpiresAtMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionLoginNonceExpiresAtMs(long sessionLoginNonceExpiresAtMs) {
|
||||||
|
this.sessionLoginNonceExpiresAtMs = sessionLoginNonceExpiresAtMs;
|
||||||
|
}
|
||||||
|
|
||||||
// --- auth status ---
|
// --- auth status ---
|
||||||
|
|
||||||
public int getAuthenticationStatus() {
|
public int getAuthenticationStatus() {
|
||||||
@ -134,9 +180,12 @@ public class ConnectionContext {
|
|||||||
activeSessionEntry = null;
|
activeSessionEntry = null;
|
||||||
|
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
sessionPwd = null;
|
|
||||||
authNonce = null;
|
authNonce = null;
|
||||||
|
|
||||||
|
sessionLoginNonce = null;
|
||||||
|
sessionLoginSessionId = null;
|
||||||
|
sessionLoginNonceExpiresAtMs = 0;
|
||||||
|
|
||||||
authenticationStatus = AUTH_STATUS_NONE;
|
authenticationStatus = AUTH_STATUS_NONE;
|
||||||
wsSession = null;
|
wsSession = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,20 +2,32 @@ package server.logic.ws_protocol.JSON;
|
|||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler;
|
|
||||||
|
// --- NEW v2 session login ---
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionChallenge_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionLogin_Handler;
|
||||||
|
|
||||||
|
// --- auth entities ---
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_RefreshSession_Request;
|
|
||||||
|
// --- NEW v2 entities ---
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.Net_GetUserParam_Handler;
|
import server.logic.ws_protocol.JSON.handlers.userParams.Net_GetUserParam_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler;
|
import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Handler;
|
import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Handler;
|
||||||
@ -37,12 +49,19 @@ public final class JsonHandlerRegistry {
|
|||||||
|
|
||||||
// Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...)
|
// Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...)
|
||||||
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
|
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
|
||||||
Map.entry("RefreshSession", new Net_RefreshSession_Handler()),
|
|
||||||
Map.entry("AddUser", new Net_AddUser_Handler()),
|
Map.entry("AddUser", new Net_AddUser_Handler()),
|
||||||
|
|
||||||
|
// --- auth ---
|
||||||
Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
|
Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
|
||||||
Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()),
|
Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()),
|
||||||
Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
|
Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
|
||||||
Map.entry("ListSessions", new Net_ListSessions_Handler()),
|
Map.entry("ListSessions", new Net_ListSessions_Handler()),
|
||||||
|
|
||||||
|
// --- login to existing session in 2 steps ---
|
||||||
|
Map.entry("SessionChallenge", new Net_SessionChallenge_Handler()),
|
||||||
|
Map.entry("SessionLogin", new Net_SessionLogin_Handler()),
|
||||||
|
|
||||||
|
// --- blockchain ---
|
||||||
Map.entry("AddBlock", new Net_AddBlock_Handler()),
|
Map.entry("AddBlock", new Net_AddBlock_Handler()),
|
||||||
|
|
||||||
// --- userParams ---
|
// --- userParams ---
|
||||||
@ -55,12 +74,19 @@ public final class JsonHandlerRegistry {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
||||||
Map.entry("RefreshSession", Net_RefreshSession_Request.class),
|
|
||||||
Map.entry("AddUser", Net_AddUser_Request.class),
|
Map.entry("AddUser", Net_AddUser_Request.class),
|
||||||
|
|
||||||
|
// --- auth ---
|
||||||
Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),
|
Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),
|
||||||
Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class),
|
Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class),
|
||||||
Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
|
Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
|
||||||
Map.entry("ListSessions", Net_ListSessions_Request.class),
|
Map.entry("ListSessions", Net_ListSessions_Request.class),
|
||||||
|
|
||||||
|
// --- NEW v2 ---
|
||||||
|
Map.entry("SessionChallenge", Net_SessionChallenge_Request.class),
|
||||||
|
Map.entry("SessionLogin", Net_SessionLogin_Request.class),
|
||||||
|
|
||||||
|
// --- blockchain ---
|
||||||
Map.entry("AddBlock", Net_AddBlock_Request.class),
|
Map.entry("AddBlock", Net_AddBlock_Request.class),
|
||||||
|
|
||||||
// --- userParams ---
|
// --- userParams ---
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.auth;
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.entyties.*;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.*;
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.SolanaUsersDAO;
|
||||||
@ -13,10 +15,18 @@ import java.security.SecureRandom;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Шаг 1 авторизации: запрос выдачи временного nonce (authNonce).
|
* AuthChallenge (v2) — шаг 1 создания новой сессии.
|
||||||
*
|
*
|
||||||
* Клиент по логину просит сервер сгенерировать случайный authNonce,
|
* Логика авторизации (v2):
|
||||||
* который будет использован на втором шаге при подписи.
|
* - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
|
||||||
|
* - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
|
||||||
|
* CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
|
||||||
|
*
|
||||||
|
* Что делает:
|
||||||
|
* 1) Проверяет login.
|
||||||
|
* 2) Находит пользователя (solana_users).
|
||||||
|
* 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
|
||||||
|
* 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
|
||||||
*/
|
*/
|
||||||
public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
@ -47,9 +57,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Ищем пользователя в локальной БД
|
|
||||||
SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
|
SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||||
|
|
||||||
if (solanaUserEntry == null) {
|
if (solanaUserEntry == null) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
@ -59,21 +67,15 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Заполняем контекст пользователем
|
|
||||||
ctx.setSolanaUser(solanaUserEntry);
|
ctx.setSolanaUser(solanaUserEntry);
|
||||||
|
|
||||||
// 3.1) Отмечаем, что по этому соединению начата авторификация
|
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
|
||||||
|
|
||||||
// 4) Генерируем одноразовый authNonce = base64(32 случайных байт)
|
|
||||||
byte[] buf = new byte[32];
|
byte[] buf = new byte[32];
|
||||||
RANDOM.nextBytes(buf);
|
RANDOM.nextBytes(buf);
|
||||||
String authNonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
String authNonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||||
|
|
||||||
// Сохраняем challenge в отдельном поле authNonce
|
|
||||||
ctx.setAuthNonce(authNonce);
|
ctx.setAuthNonce(authNonce);
|
||||||
|
|
||||||
// 5) Формируем ответ
|
|
||||||
Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
|
Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
|
|||||||
@ -4,10 +4,11 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.*;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.ws.WsConnectionUtils;
|
import server.ws.WsConnectionUtils;
|
||||||
@ -18,31 +19,15 @@ import shine.db.entities.SolanaUserEntry;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Хэндлер CloseActiveSession.
|
* CloseActiveSession (v2) — закрытие текущей или другой сессии.
|
||||||
*
|
*
|
||||||
* Назначение:
|
* Логика авторизации (v2):
|
||||||
* - закрыть одну из активных сессий пользователя:
|
* - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
|
||||||
* * либо явно указанную в sessionId,
|
* - Никаких подписей и AUTH_IN_PROGRESS здесь больше нет.
|
||||||
* * либо текущую (если sessionId не задана).
|
|
||||||
*
|
|
||||||
* Допустимые состояния:
|
|
||||||
* - AUTH_STATUS_USER:
|
|
||||||
* * timeMs / signatureB64 могут быть пустыми.
|
|
||||||
* * Достаточно факта текущей авторизации.
|
|
||||||
*
|
|
||||||
* - AUTH_STATUS_AUTH_IN_PROGRESS:
|
|
||||||
* * требуется проверка подписи Ed25519 над строкой
|
|
||||||
* "AUTHORIFICATED:" + timeMs + authNonce
|
|
||||||
* (authNonce взят на шаге AuthChallenge и хранится в ctx.authNonce).
|
|
||||||
* * Если подпись корректна, можно закрывать сессию даже до полноценной
|
|
||||||
* установки новой сессии.
|
|
||||||
*
|
*
|
||||||
* Закрытие:
|
* Закрытие:
|
||||||
* - запись ActiveSession удаляется из БД;
|
* - удаляем запись из БД
|
||||||
* - если по этой sessionId есть активное WebSocket-подключение:
|
* - если по sessionId есть активный WS — закрываем его
|
||||||
* * если это ДРУГОЕ подключение — оно закрывается сразу;
|
|
||||||
* * если это ТЕКУЩЕЕ подключение — сначала отправляется ответ 200,
|
|
||||||
* а закрытие выполняется в отдельном потоке с небольшой задержкой.
|
|
||||||
*/
|
*/
|
||||||
public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
@ -52,100 +37,24 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
|||||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
|
Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
|
||||||
|
|
||||||
if (ctx == null || ctx.getSolanaUser() == null) {
|
if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.UNVERIFIED,
|
WireCodes.Status.UNVERIFIED,
|
||||||
"NOT_AUTHENTICATED",
|
"NOT_AUTHENTICATED",
|
||||||
"Операция доступна только в состоянии авторизации или авторификации"
|
"Операция доступна только для авторизованных пользователей"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry user = ctx.getSolanaUser();
|
SolanaUserEntry user = ctx.getSolanaUser();
|
||||||
String currentLogin = user.getLogin();
|
String currentLogin = user.getLogin();
|
||||||
|
|
||||||
int authStatus = ctx.getAuthenticationStatus();
|
|
||||||
if (authStatus != ConnectionContext.AUTH_STATUS_USER
|
|
||||||
&& authStatus != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
|
||||||
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.UNVERIFIED,
|
|
||||||
"BAD_AUTH_STATUS",
|
|
||||||
"Операция CloseActiveSession недоступна в текущем статусе аутентификации"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если мы ещё на шаге AUTH_IN_PROGRESS — проверяем подпись
|
|
||||||
if (authStatus == ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
|
||||||
String authNonce = ctx.getAuthNonce();
|
|
||||||
if (authNonce == null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"NO_STEP1_CONTEXT",
|
|
||||||
"Шаг 1 авторизации не был корректно выполнен для данного соединения"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
long timeMs = req.getTimeMs();
|
|
||||||
String signatureB64 = req.getSignatureB64();
|
|
||||||
|
|
||||||
if (signatureB64 == null || signatureB64.isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"EMPTY_SIGNATURE",
|
|
||||||
"Подпись обязательна при статусе AUTH_IN_PROGRESS"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
long nowMs = System.currentTimeMillis();
|
|
||||||
long diff = Math.abs(nowMs - timeMs);
|
|
||||||
if (diff > Net_CreateAuthSession__Handler.ALLOWED_SKEW_MS) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"TIME_SKEW",
|
|
||||||
"Время клиента отличается от сервера более чем на 30 секунд"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean sigOk;
|
|
||||||
try {
|
|
||||||
sigOk = Net_CreateAuthSession__Handler.verifyAuthorificatedSignature(
|
|
||||||
user,
|
|
||||||
authNonce,
|
|
||||||
timeMs,
|
|
||||||
signatureB64
|
|
||||||
);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_BASE64",
|
|
||||||
"Некорректный формат Base64 для ключа или подписи"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sigOk) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.UNVERIFIED,
|
|
||||||
"BAD_SIGNATURE",
|
|
||||||
"Подпись не прошла проверку"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Определяем, какую sessionId закрывать
|
|
||||||
String targetSessionId = req.getSessionId();
|
String targetSessionId = req.getSessionId();
|
||||||
if (targetSessionId == null || targetSessionId.isBlank()) {
|
if (targetSessionId == null || targetSessionId.isBlank()) {
|
||||||
// Если sessionId не передана — берём текущую активную
|
if (ctx.getSessionId() != null && !ctx.getSessionId().isBlank()) {
|
||||||
if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
|
|
||||||
targetSessionId = ctx.getActiveSession().getSessionId();
|
|
||||||
} else if (ctx.getSessionId() != null) {
|
|
||||||
targetSessionId = ctx.getSessionId();
|
targetSessionId = ctx.getSessionId();
|
||||||
|
} else if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
|
||||||
|
targetSessionId = ctx.getActiveSession().getSessionId();
|
||||||
} else {
|
} else {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
@ -156,10 +65,9 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
|
||||||
ActiveSessionEntry targetSession;
|
ActiveSessionEntry targetSession;
|
||||||
try {
|
try {
|
||||||
targetSession = sessionsDao.getBySessionId(targetSessionId);
|
targetSession = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
|
log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
@ -190,50 +98,31 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
|
boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
|
||||||
|
|
||||||
// Пытаемся удалить сессию из БД и закрыть соответствующее подключение
|
|
||||||
closeActiveSession(targetSessionId, ctx, isCurrentSession);
|
closeActiveSession(targetSessionId, ctx, isCurrentSession);
|
||||||
|
|
||||||
// Ответ OK (payload станет {} в JsonInboundProcessor)
|
|
||||||
Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
|
Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
|
||||||
// Для текущей сессии WebSocket будет закрыт чуть позже в отдельном потоке,
|
|
||||||
// чтобы этот ответ успел уйти.
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Закрытие активной сессии:
|
|
||||||
* - удаление записи из БД;
|
|
||||||
* - закрытие WebSocket-подключения, если оно существует.
|
|
||||||
*
|
|
||||||
* @param targetSessionId идентификатор сессии, которую надо закрыть
|
|
||||||
* @param currentCtx контекст текущего подключения (которое вызвало запрос)
|
|
||||||
* @param isCurrentSession true, если закрывается "эта же" сессия
|
|
||||||
*/
|
|
||||||
private void closeActiveSession(String targetSessionId,
|
private void closeActiveSession(String targetSessionId,
|
||||||
ConnectionContext currentCtx,
|
ConnectionContext currentCtx,
|
||||||
boolean isCurrentSession) {
|
boolean isCurrentSession) {
|
||||||
|
|
||||||
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
|
||||||
try {
|
try {
|
||||||
sessionsDao.deleteBySessionId(targetSessionId);
|
ActiveSessionsDAO.getInstance().deleteBySessionId(targetSessionId);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
|
log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
|
||||||
// Логируем, но считаем, что для клиента сессия всё равно должна быть недействительна.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionContext ctxToClose =
|
ConnectionContext ctxToClose =
|
||||||
ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
|
ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
|
||||||
|
|
||||||
if (ctxToClose == null) {
|
if (ctxToClose == null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCurrentSession && ctxToClose == currentCtx) {
|
if (isCurrentSession && ctxToClose == currentCtx) {
|
||||||
// Это текущее подключение: закрываем после отправки ответа.
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
|
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
|
||||||
WsConnectionUtils.closeConnection(
|
WsConnectionUtils.closeConnection(
|
||||||
@ -243,7 +132,6 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}, "CloseSession-" + targetSessionId).start();
|
}, "CloseSession-" + targetSessionId).start();
|
||||||
} else {
|
} else {
|
||||||
// Другая сессия — можно закрыть сразу
|
|
||||||
WsConnectionUtils.closeConnection(
|
WsConnectionUtils.closeConnection(
|
||||||
ctxToClose,
|
ctxToClose,
|
||||||
4000,
|
4000,
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.auth;
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.eclipse.jetty.websocket.api.Session;
|
|
||||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.*;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.ws.WsConnectionUtils;
|
import server.ws.WsConnectionUtils;
|
||||||
@ -20,37 +21,37 @@ import shine.geo.GeoLookupService;
|
|||||||
import utils.crypto.Ed25519Util;
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
|
||||||
|
*
|
||||||
|
* Логика авторизации (v2):
|
||||||
|
* - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
|
||||||
|
* - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
|
||||||
|
* отправляет на сервер ТОЛЬКО sessionPubKeyB64.
|
||||||
|
* - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
|
||||||
|
*
|
||||||
|
* Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
|
||||||
|
* AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
|
||||||
|
*
|
||||||
|
* На выходе:
|
||||||
|
* - создаётся запись active_sessions
|
||||||
|
* - ctx становится AUTH_STATUS_USER (вход выполнен как "текущая сессия")
|
||||||
|
* - ответ: sessionId
|
||||||
|
*/
|
||||||
public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
|
private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
|
||||||
|
|
||||||
private static final SecureRandom RANDOM = new SecureRandom();
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
|
||||||
public static final long ALLOWED_SKEW_MS = 30_000L;
|
public static final long ALLOWED_SKEW_MS = 30_000L;
|
||||||
|
|
||||||
public static boolean verifyAuthorificatedSignature(
|
|
||||||
SolanaUserEntry user,
|
|
||||||
String authNonce,
|
|
||||||
long timeMs,
|
|
||||||
String signatureB64
|
|
||||||
) throws IllegalArgumentException {
|
|
||||||
|
|
||||||
String pubKeyB64 = user.getDeviceKey();
|
|
||||||
byte[] publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64);
|
|
||||||
byte[] signature64 = Base64.getDecoder().decode(signatureB64);
|
|
||||||
|
|
||||||
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
|
||||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
return Ed25519Util.verify(preimage, signature64, publicKey32);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
|
|
||||||
Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
|
Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
|
||||||
|
|
||||||
if (ctx == null
|
if (ctx == null
|
||||||
@ -93,6 +94,43 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String sessionPubKeyB64 = req.getSessionPubKeyB64();
|
||||||
|
if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
|
||||||
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"EMPTY_SESSION_PUBKEY",
|
||||||
|
"Пустой sessionPubKeyB64"
|
||||||
|
);
|
||||||
|
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверим, что ключ декодируется в 32 байта
|
||||||
|
byte[] sessionPubKey32;
|
||||||
|
try {
|
||||||
|
sessionPubKey32 = decodeBase64Any(sessionPubKeyB64);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_BASE64",
|
||||||
|
"Некорректный base64 в sessionPubKeyB64"
|
||||||
|
);
|
||||||
|
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
if (sessionPubKey32.length != 32) {
|
||||||
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_SESSION_PUBKEY_LEN",
|
||||||
|
"sessionPubKey должен быть 32 байта"
|
||||||
|
);
|
||||||
|
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
String signatureB64 = req.getSignatureB64();
|
String signatureB64 = req.getSignatureB64();
|
||||||
if (signatureB64 == null || signatureB64.isBlank()) {
|
if (signatureB64 == null || signatureB64.isBlank()) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
@ -107,7 +145,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
long timeMs = req.getTimeMs();
|
long timeMs = req.getTimeMs();
|
||||||
long nowMs = System.currentTimeMillis();
|
long nowMs = System.currentTimeMillis();
|
||||||
|
|
||||||
long diff = Math.abs(nowMs - timeMs);
|
long diff = Math.abs(nowMs - timeMs);
|
||||||
if (diff > ALLOWED_SKEW_MS) {
|
if (diff > ALLOWED_SKEW_MS) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
@ -125,15 +162,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
String pubKeyB64 = user.getDeviceKey();
|
String devicePubKeyB64 = user.getDeviceKey();
|
||||||
if (pubKeyB64 == null || pubKeyB64.isBlank()) {
|
if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.BAD_REQUEST,
|
WireCodes.Status.BAD_REQUEST,
|
||||||
"NO_PUBKEY1",
|
"NO_DEVICE_KEY",
|
||||||
"Отсутствует публичный ключ pubkey1 для пользователя"
|
"Отсутствует deviceKey у пользователя"
|
||||||
);
|
);
|
||||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no pubkey");
|
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no deviceKey");
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +178,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
boolean sigOk;
|
boolean sigOk;
|
||||||
try {
|
try {
|
||||||
sigOk = verifyAuthorificatedSignature(user, authNonce, timeMs, signatureB64);
|
sigOk = verifyCreateSessionSignature(
|
||||||
|
user,
|
||||||
|
login,
|
||||||
|
authNonce,
|
||||||
|
timeMs,
|
||||||
|
sessionPubKeyB64,
|
||||||
|
storagePwd,
|
||||||
|
signatureB64
|
||||||
|
);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
@ -164,9 +209,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Генерируем настоящий секрет сессии (sessionPwd) и sessionId ---
|
// --- генерируем sessionId ---
|
||||||
String newSessionPwd = generateRandomSecret();
|
String sessionId = generateRandom32B64Url();
|
||||||
String sessionId = generateRandomSessionId();
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
// --- Сбор данных о клиенте (IP, UA, язык) ---
|
// --- Сбор данных о клиенте (IP, UA, язык) ---
|
||||||
@ -174,11 +218,12 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
|
String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
|
||||||
String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
|
String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
|
||||||
|
|
||||||
String clientIp = null;
|
String clientIp = "";
|
||||||
if (wsSession != null) {
|
if (wsSession != null) {
|
||||||
clientIp = ClientInfoService.extractClientIp(wsSession);
|
String ip = ClientInfoService.extractClientIp(wsSession);
|
||||||
|
if (ip != null) clientIp = ip;
|
||||||
|
|
||||||
if (clientIp != null && !clientIp.isBlank()) {
|
if (!clientIp.isBlank()) {
|
||||||
try {
|
try {
|
||||||
GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
|
GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -186,7 +231,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (clientIp == null) clientIp = "";
|
|
||||||
|
|
||||||
// --- создаём запись ActiveSession и сохраняем в БД ---
|
// --- создаём запись ActiveSession и сохраняем в БД ---
|
||||||
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
|
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
|
||||||
@ -196,7 +240,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
activeSessionEntry = new ActiveSessionEntry(
|
activeSessionEntry = new ActiveSessionEntry(
|
||||||
sessionId,
|
sessionId,
|
||||||
login,
|
login,
|
||||||
newSessionPwd,
|
sessionPubKeyB64, // session_key (pubkey)
|
||||||
storagePwd,
|
storagePwd,
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
@ -225,8 +269,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
// --- обновляем контекст ---
|
// --- обновляем контекст ---
|
||||||
ctx.setActiveSession(activeSessionEntry);
|
ctx.setActiveSession(activeSessionEntry);
|
||||||
ctx.setSessionId(sessionId);
|
ctx.setSessionId(sessionId);
|
||||||
ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии
|
ctx.setAuthNonce(null);
|
||||||
ctx.setAuthNonce(null); // одноразовый nonce больше не нужен
|
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||||
|
|
||||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||||
@ -237,25 +280,40 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setSessionId(sessionId);
|
resp.setSessionId(sessionId);
|
||||||
resp.setSessionPwd(newSessionPwd);
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static boolean verifyCreateSessionSignature(
|
||||||
* Генерация случайного sessionId: base64-строка от 32 байт.
|
SolanaUserEntry user,
|
||||||
*/
|
String login,
|
||||||
private String generateRandomSessionId() {
|
String authNonce,
|
||||||
|
long timeMs,
|
||||||
|
String sessionPubKeyB64,
|
||||||
|
String storagePwd,
|
||||||
|
String signatureB64
|
||||||
|
) throws IllegalArgumentException {
|
||||||
|
|
||||||
|
byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
|
||||||
|
byte[] signature64 = decodeBase64Any(signatureB64);
|
||||||
|
|
||||||
|
String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce + ":" + sessionPubKeyB64 + ":" + storagePwd;
|
||||||
|
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
return Ed25519Util.verify(preimage, signature64, publicKey32);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateRandom32B64Url() {
|
||||||
byte[] buf = new byte[32];
|
byte[] buf = new byte[32];
|
||||||
RANDOM.nextBytes(buf);
|
RANDOM.nextBytes(buf);
|
||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
|
||||||
* Генерация случайного секрета (sessionPwd): base64-строка от 32 байт.
|
// сначала url-safe, потом обычный
|
||||||
*/
|
try {
|
||||||
private String generateRandomSecret() {
|
return Base64.getUrlDecoder().decode(s);
|
||||||
byte[] buf = new byte[32];
|
} catch (IllegalArgumentException ignore) {
|
||||||
RANDOM.nextBytes(buf);
|
return Base64.getDecoder().decode(s);
|
||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,12 +3,13 @@ package server.logic.ws_protocol.JSON.handlers.auth;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.*;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.ActiveSessionsDAO;
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
@ -20,16 +21,11 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Хэндлер ListSessions.
|
* ListSessions (v2) — список активных сессий.
|
||||||
*
|
*
|
||||||
* Назначение:
|
* Логика авторизации (v2):
|
||||||
* - вернуть список всех активных сессий текущего пользователя
|
* - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
|
||||||
* (по loginId из ctx/solanaUser).
|
* - Никаких подписей здесь больше нет.
|
||||||
*
|
|
||||||
* Безопасность:
|
|
||||||
* - анонимный клиент → NOT_AUTHENTICATED (UNVERIFIED);
|
|
||||||
* - AUTH_STATUS_USER → достаточно факта авторизации;
|
|
||||||
* - AUTH_STATUS_AUTH_IN_PROGRESS → требуется подпись, как в CreateAuthSession/CloseActiveSession.
|
|
||||||
*/
|
*/
|
||||||
public class Net_ListSessions_Handler implements JsonMessageHandler {
|
public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
@ -39,8 +35,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
|||||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
|
Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
|
||||||
|
|
||||||
// 1) Проверяем, что вообще есть пользователь в контексте
|
if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||||
if (ctx == null || ctx.getSolanaUser() == null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.UNVERIFIED,
|
WireCodes.Status.UNVERIFIED,
|
||||||
@ -52,81 +47,6 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
|||||||
SolanaUserEntry user = ctx.getSolanaUser();
|
SolanaUserEntry user = ctx.getSolanaUser();
|
||||||
String currentLogin = user.getLogin();
|
String currentLogin = user.getLogin();
|
||||||
|
|
||||||
int authStatus = ctx.getAuthenticationStatus();
|
|
||||||
if (authStatus != ConnectionContext.AUTH_STATUS_USER
|
|
||||||
&& authStatus != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
|
||||||
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.UNVERIFIED,
|
|
||||||
"BAD_AUTH_STATUS",
|
|
||||||
"Операция ListSessions недоступна в текущем статусе аутентификации"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Если мы ещё на шаге AUTH_IN_PROGRESS — проверяем подпись
|
|
||||||
if (authStatus == ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
|
||||||
String authNonce = ctx.getAuthNonce();
|
|
||||||
if (authNonce == null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"NO_STEP1_CONTEXT",
|
|
||||||
"Шаг 1 авторизации не был корректно выполнен для данного соединения"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
long timeMs = req.getTimeMs();
|
|
||||||
String signatureB64 = req.getSignatureB64();
|
|
||||||
|
|
||||||
if (signatureB64 == null || signatureB64.isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"EMPTY_SIGNATURE",
|
|
||||||
"Подпись обязательна при статусе AUTH_IN_PROGRESS"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
long nowMs = System.currentTimeMillis();
|
|
||||||
long diff = Math.abs(nowMs - timeMs);
|
|
||||||
if (diff > Net_CreateAuthSession__Handler.ALLOWED_SKEW_MS) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"TIME_SKEW",
|
|
||||||
"Время клиента отличается от сервера более чем на 30 секунд"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean sigOk;
|
|
||||||
try {
|
|
||||||
sigOk = Net_CreateAuthSession__Handler.verifyAuthorificatedSignature(
|
|
||||||
user,
|
|
||||||
authNonce,
|
|
||||||
timeMs,
|
|
||||||
signatureB64
|
|
||||||
);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_BASE64",
|
|
||||||
"Некорректный формат Base64 для ключа или подписи"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sigOk) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.UNVERIFIED,
|
|
||||||
"BAD_SIGNATURE",
|
|
||||||
"Подпись не прошла проверку"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Тянем все активные сессии пользователя из БД
|
|
||||||
List<ActiveSessionEntry> sessions;
|
List<ActiveSessionEntry> sessions;
|
||||||
try {
|
try {
|
||||||
sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
|
sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
|
||||||
@ -140,10 +60,9 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Собираем DTO с геолокацией
|
|
||||||
List<SessionInfo> resultList = new ArrayList<>();
|
List<SessionInfo> resultList = new ArrayList<>();
|
||||||
for (ActiveSessionEntry s : sessions) {
|
for (ActiveSessionEntry s : sessions) {
|
||||||
SessionInfo info = new Net_ListSessions_Response.SessionInfo();
|
SessionInfo info = new SessionInfo();
|
||||||
info.setSessionId(s.getSessionId());
|
info.setSessionId(s.getSessionId());
|
||||||
info.setClientInfoFromClient(s.getClientInfoFromClient());
|
info.setClientInfoFromClient(s.getClientInfoFromClient());
|
||||||
info.setClientInfoFromRequest(s.getClientInfoFromRequest());
|
info.setClientInfoFromRequest(s.getClientInfoFromRequest());
|
||||||
@ -156,7 +75,6 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
|||||||
resultList.add(info);
|
resultList.add(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Формируем ответ
|
|
||||||
Net_ListSessions_Response resp = new Net_ListSessions_Response();
|
Net_ListSessions_Response resp = new Net_ListSessions_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
|
|||||||
@ -1,200 +1,30 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.auth;
|
//package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
//
|
||||||
import org.slf4j.Logger;
|
//import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import org.slf4j.LoggerFactory;
|
//import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
//import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
//import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.*;
|
//import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_RefreshSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
//import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
//import server.logic.ws_protocol.WireCodes;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
//
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
///**
|
||||||
import server.logic.ws_protocol.WireCodes;
|
// * RefreshSession (v2) — ОТКЛЮЧЕН.
|
||||||
import shine.db.dao.ActiveSessionsDAO;
|
// *
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
// * Раньше это был "короткий вход" (1 запрос) по sessionId+sessionPwd.
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
// * Теперь вход всегда 2 шага: SessionChallenge -> SessionLogin (подпись sessionKey).
|
||||||
import shine.db.entities.SolanaUserEntry;
|
// */
|
||||||
import shine.geo.ClientInfoService;
|
//public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
||||||
import shine.geo.GeoLookupService;
|
//
|
||||||
|
// @Override
|
||||||
import java.sql.SQLException;
|
// public Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception {
|
||||||
|
// Net_RefreshSession_Request req = (Net_RefreshSession_Request) request;
|
||||||
/**
|
//
|
||||||
* Хэндлер RefreshSession.
|
// return NetExceptionResponseFactory.error(
|
||||||
*
|
// req,
|
||||||
* При успешной проверке sessionId + sessionPwd:
|
// WireCodes.Status.GONE, // 410
|
||||||
* - подтягивает пользователя по loginId из сессии;
|
// "DISABLED_V2",
|
||||||
* - заполняет ConnectionContext;
|
// "RefreshSession отключён в v2. Используй SessionChallenge + SessionLogin."
|
||||||
* - обновляет lastAuthirificatedAtMs и метаданные сессии в БД;
|
// );
|
||||||
* - возвращает storagePwd в payload.
|
// }
|
||||||
*/
|
//}
|
||||||
public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_RefreshSession_Handler.class);
|
|
||||||
|
|
||||||
// максимум 50 символов для clientInfo от клиента
|
|
||||||
private static final int CLIENT_INFO_MAX_LEN = 50;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception {
|
|
||||||
Net_RefreshSession_Request req = (Net_RefreshSession_Request) request;
|
|
||||||
|
|
||||||
String sessionId = req.getSessionId();
|
|
||||||
String sessionPwd = req.getSessionPwd();
|
|
||||||
String clientInfoFromClient = trimClientInfo(req.getClientInfo());
|
|
||||||
|
|
||||||
if (sessionId == null || sessionId.isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_SESSION_ID",
|
|
||||||
"Пустой идентификатор сессии"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessionPwd == null || sessionPwd.isEmpty()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_SESSION_PWD",
|
|
||||||
"Пустой пароль сессии"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
|
||||||
ActiveSessionEntry session;
|
|
||||||
try {
|
|
||||||
session = sessionsDao.getBySessionId(sessionId);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Ошибка БД при поиске сессии sessionId={}", sessionId, e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
|
||||||
"DB_ERROR",
|
|
||||||
"Ошибка доступа к базе данных"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session == null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.UNVERIFIED,
|
|
||||||
"SESSION_NOT_FOUND",
|
|
||||||
"Сессия не найдена"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String dbPwd = session.getSessionPwd();
|
|
||||||
if (dbPwd == null || !dbPwd.equals(sessionPwd)) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.UNVERIFIED,
|
|
||||||
"SESSION_PWD_MISMATCH",
|
|
||||||
"Неверный пароль сессии"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- вытаскиваем пользователя по login из сессии ---
|
|
||||||
SolanaUserEntry solanaUserEntry;
|
|
||||||
String login = session.getLogin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
|
||||||
solanaUserEntry = usersDao.getByLogin(login);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Ошибка БД при поиске пользователя по login={} из сессии", login, e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
|
||||||
"DB_ERROR_USER_LOOKUP",
|
|
||||||
"Ошибка доступа к базе данных при получении пользователя для сессии"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solanaUserEntry == null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.UNVERIFIED,
|
|
||||||
"USER_NOT_FOUND_FOR_SESSION",
|
|
||||||
"Пользователь для данной сессии не найден"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- собираем данные о клиенте из WebSocket-запроса ---
|
|
||||||
String clientIp = null;
|
|
||||||
String clientInfoFromRequest = null;
|
|
||||||
String userLanguage = null;
|
|
||||||
|
|
||||||
if (ctx != null && ctx.getWsSession() != null) {
|
|
||||||
// Нормальное получение IP-адреса клиента
|
|
||||||
clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
|
|
||||||
|
|
||||||
// Сделать запрос геолокации и никуда её не сохранять:
|
|
||||||
// вызов с кэшированием в БД, нужно только для прогрева кэша.
|
|
||||||
if (clientIp != null && !clientIp.isBlank()) {
|
|
||||||
try {
|
|
||||||
GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Геолокация не критична, просто логируем на debug/trace при желании
|
|
||||||
log.debug("Geo lookup failed for ip={}", clientIp, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
|
|
||||||
userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
|
|
||||||
}
|
|
||||||
|
|
||||||
long nowMs = System.currentTimeMillis();
|
|
||||||
|
|
||||||
// --- обновляем запись в БД (lastAuth + мета) ---
|
|
||||||
try {
|
|
||||||
sessionsDao.updateOnRefresh(
|
|
||||||
sessionId,
|
|
||||||
nowMs,
|
|
||||||
clientIp,
|
|
||||||
clientInfoFromClient,
|
|
||||||
clientInfoFromRequest,
|
|
||||||
userLanguage
|
|
||||||
);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Ошибка БД при обновлении метаданных сессии sessionId={}", sessionId, e);
|
|
||||||
// не роняем авторизацию, но логируем
|
|
||||||
}
|
|
||||||
|
|
||||||
// Также обновим объект session в памяти (если дальше кто-то его использует)
|
|
||||||
session.setLastAuthirificatedAtMs(nowMs);
|
|
||||||
session.setClientIp(clientIp);
|
|
||||||
session.setClientInfoFromClient(clientInfoFromClient);
|
|
||||||
session.setClientInfoFromRequest(clientInfoFromRequest);
|
|
||||||
session.setUserLanguage(userLanguage);
|
|
||||||
|
|
||||||
// --- обновляем контекст соединения ---
|
|
||||||
if (ctx != null) {
|
|
||||||
ctx.setActiveSession(session);
|
|
||||||
ctx.setSolanaUser(solanaUserEntry);
|
|
||||||
ctx.setSessionId(sessionId);
|
|
||||||
ctx.setSessionPwd(sessionPwd);
|
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
|
||||||
|
|
||||||
// Регистрируем это подключение в глобальном реестре активных соединений
|
|
||||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ответ OK + storagePwd ---
|
|
||||||
Net_RefreshSession_Response resp = new Net_RefreshSession_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
resp.setStoragePwd(session.getStoragePwd());
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String trimClientInfo(String info) {
|
|
||||||
if (info == null) return null;
|
|
||||||
info = info.trim();
|
|
||||||
if (info.length() > CLIENT_INFO_MAX_LEN) {
|
|
||||||
return info.substring(0, CLIENT_INFO_MAX_LEN);
|
|
||||||
}
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SessionChallenge (v2) — шаг 1 входа в существующую сессию.
|
||||||
|
*
|
||||||
|
* Логика авторизации (v2):
|
||||||
|
* - Вход в существующую сессию ВСЕГДА в 2 шага:
|
||||||
|
* 1) SessionChallenge(sessionId) -> nonce
|
||||||
|
* 2) SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
|
||||||
|
*
|
||||||
|
* Что делает:
|
||||||
|
* - Проверяет, что sessionId существует в БД.
|
||||||
|
* - Генерирует одноразовый nonce (base64url(32)), сохраняет его в ctx:
|
||||||
|
* ctx.sessionLoginNonce, ctx.sessionLoginSessionId, ctx.sessionLoginNonceExpiresAtMs.
|
||||||
|
*/
|
||||||
|
public class Net_SessionChallenge_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
private static final long NONCE_TTL_MS = 60_000L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_SessionChallenge_Request req = (Net_SessionChallenge_Request) baseReq;
|
||||||
|
|
||||||
|
String sessionId = req.getSessionId();
|
||||||
|
if (sessionId == null || sessionId.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"EMPTY_SESSION_ID",
|
||||||
|
"Пустой sessionId"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveSessionEntry session;
|
||||||
|
try {
|
||||||
|
session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"DB_ERROR",
|
||||||
|
"Ошибка доступа к базе данных"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"SESSION_NOT_FOUND",
|
||||||
|
"Сессия не найдена"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buf = new byte[32];
|
||||||
|
RANDOM.nextBytes(buf);
|
||||||
|
String nonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
ctx.setSessionLoginNonce(nonce);
|
||||||
|
ctx.setSessionLoginSessionId(sessionId);
|
||||||
|
ctx.setSessionLoginNonceExpiresAtMs(now + NONCE_TTL_MS);
|
||||||
|
|
||||||
|
Net_SessionChallenge_Response resp = new Net_SessionChallenge_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setNonce(nonce);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,269 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
|
import shine.db.dao.SolanaUsersDAO;
|
||||||
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
|
import shine.db.entities.SolanaUserEntry;
|
||||||
|
import shine.geo.ClientInfoService;
|
||||||
|
import shine.geo.GeoLookupService;
|
||||||
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SessionLogin (v2) — шаг 2 входа в существующую сессию (по sessionKey).
|
||||||
|
*
|
||||||
|
* Логика авторизации (v2):
|
||||||
|
* - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
|
||||||
|
* - SessionLogin проверяет подпись sessionKey над строкой:
|
||||||
|
* SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||||||
|
* - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
|
||||||
|
*
|
||||||
|
* При успехе:
|
||||||
|
* - ctx становится AUTH_STATUS_USER
|
||||||
|
* - обновляем метаданные сессии (lastAuth + clientIp + clientInfo + lang)
|
||||||
|
* - возвращаем storagePwd
|
||||||
|
*/
|
||||||
|
public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
|
||||||
|
|
||||||
|
private static final long ALLOWED_SKEW_MS = 30_000L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;
|
||||||
|
|
||||||
|
String sessionId = req.getSessionId();
|
||||||
|
if (sessionId == null || sessionId.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"EMPTY_SESSION_ID",
|
||||||
|
"Пустой sessionId"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// проверка челленджа
|
||||||
|
if (ctx.getSessionLoginNonce() == null
|
||||||
|
|| ctx.getSessionLoginSessionId() == null
|
||||||
|
|| System.currentTimeMillis() > ctx.getSessionLoginNonceExpiresAtMs()) {
|
||||||
|
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"NO_CHALLENGE",
|
||||||
|
"Нет активного SessionChallenge или nonce истёк"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionId.equals(ctx.getSessionLoginSessionId())) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"SESSION_ID_MISMATCH",
|
||||||
|
"nonce был выдан для другого sessionId"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long timeMs = req.getTimeMs();
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"TIME_SKEW",
|
||||||
|
"Время клиента отличается от сервера более чем на 30 секунд"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String signatureB64 = req.getSignatureB64();
|
||||||
|
if (signatureB64 == null || signatureB64.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"EMPTY_SIGNATURE",
|
||||||
|
"Пустая подпись"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveSessionEntry session;
|
||||||
|
try {
|
||||||
|
session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"DB_ERROR",
|
||||||
|
"Ошибка доступа к базе данных"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"SESSION_NOT_FOUND",
|
||||||
|
"Сессия не найдена"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String sessionPubKeyB64 = session.getSessionKey(); // это pubKey
|
||||||
|
if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"NO_SESSION_KEY",
|
||||||
|
"В сессии не задан session_key"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String nonce = ctx.getSessionLoginNonce();
|
||||||
|
|
||||||
|
boolean sigOk;
|
||||||
|
try {
|
||||||
|
sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_BASE64",
|
||||||
|
"Некорректный Base64 для ключа/подписи"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sigOk) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"BAD_SIGNATURE",
|
||||||
|
"Подпись не прошла проверку"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// сжигаем nonce
|
||||||
|
ctx.setSessionLoginNonce(null);
|
||||||
|
ctx.setSessionLoginSessionId(null);
|
||||||
|
ctx.setSessionLoginNonceExpiresAtMs(0);
|
||||||
|
|
||||||
|
// подтягиваем пользователя
|
||||||
|
SolanaUserEntry user;
|
||||||
|
try {
|
||||||
|
user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"DB_ERROR_USER_LOOKUP",
|
||||||
|
"Ошибка доступа к базе данных при получении пользователя"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"USER_NOT_FOUND_FOR_SESSION",
|
||||||
|
"Пользователь для данной сессии не найден"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// обновление метаданных
|
||||||
|
String clientInfoFromClient = req.getClientInfo();
|
||||||
|
if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
|
||||||
|
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientIp = null;
|
||||||
|
String clientInfoFromRequest = null;
|
||||||
|
String userLanguage = null;
|
||||||
|
|
||||||
|
if (ctx.getWsSession() != null) {
|
||||||
|
clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
|
||||||
|
clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
|
||||||
|
userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
|
||||||
|
|
||||||
|
if (clientIp != null && !clientIp.isBlank()) {
|
||||||
|
try {
|
||||||
|
GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Geo lookup failed for ip={}", clientIp, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
ActiveSessionsDAO.getInstance().updateOnRefresh(
|
||||||
|
sessionId,
|
||||||
|
now,
|
||||||
|
clientIp,
|
||||||
|
clientInfoFromClient,
|
||||||
|
clientInfoFromRequest,
|
||||||
|
userLanguage
|
||||||
|
);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Ошибка БД при updateOnRefresh sessionId={}", sessionId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setLastAuthirificatedAtMs(now);
|
||||||
|
session.setClientIp(clientIp);
|
||||||
|
session.setClientInfoFromClient(clientInfoFromClient);
|
||||||
|
session.setClientInfoFromRequest(clientInfoFromRequest);
|
||||||
|
session.setUserLanguage(userLanguage);
|
||||||
|
|
||||||
|
// ctx
|
||||||
|
ctx.setActiveSession(session);
|
||||||
|
ctx.setSolanaUser(user);
|
||||||
|
ctx.setSessionId(sessionId);
|
||||||
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||||
|
|
||||||
|
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||||
|
|
||||||
|
// ответ
|
||||||
|
Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setStoragePwd(session.getStoragePwd());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean verifySessionLoginSignature(
|
||||||
|
String sessionPubKeyB64,
|
||||||
|
String sessionId,
|
||||||
|
long timeMs,
|
||||||
|
String nonce,
|
||||||
|
String signatureB64
|
||||||
|
) throws IllegalArgumentException {
|
||||||
|
|
||||||
|
byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);
|
||||||
|
byte[] signature64 = decodeBase64Any(signatureB64);
|
||||||
|
|
||||||
|
String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
|
||||||
|
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
return Ed25519Util.verify(preimage, signature64, publicKey32);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
|
||||||
|
try {
|
||||||
|
return Base64.getUrlDecoder().decode(s);
|
||||||
|
} catch (IllegalArgumentException ignore) {
|
||||||
|
return Base64.getDecoder().decode(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,35 +5,20 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|||||||
/**
|
/**
|
||||||
* Запрос CloseActiveSession — закрытие активной сессии пользователя.
|
* Запрос CloseActiveSession — закрытие активной сессии пользователя.
|
||||||
*
|
*
|
||||||
* Допустимые режимы:
|
* Новая логика (v2):
|
||||||
|
* - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
|
||||||
|
* - Никаких подписей и "AUTH_IN_PROGRESS" здесь больше нет.
|
||||||
*
|
*
|
||||||
* 1) Пользователь уже авторизован (AUTH_STATUS_USER):
|
* payload:
|
||||||
* - поле sessionId:
|
* {
|
||||||
* * если заполнено — закрывается указанная сессия пользователя;
|
* "sessionId": "..." // опционально; если пусто — закрываем текущую
|
||||||
* * если пустое — закрывается текущая авторизованная сессия
|
* }
|
||||||
* (та, в рамках которой выполняется запрос).
|
|
||||||
* - поля timeMs и signatureB64 могут быть пустыми и игнорируются.
|
|
||||||
*
|
|
||||||
* 2) Пользователь в статусе AUTH_STATUS_AUTH_IN_PROGRESS:
|
|
||||||
* - требуется дополнительно подтвердить владение ключом:
|
|
||||||
* * timeMs — время на клиенте (мс с 1970-01-01),
|
|
||||||
* * signatureB64 — подпись Ed25519 над строкой
|
|
||||||
* "AUTHORIFICATED:" + timeMs + authNonce.
|
|
||||||
* - authNonce берётся из шага 1 (AuthChallenge) и хранится в ctx.authNonce.
|
|
||||||
* - если подпись корректна, сервер закрывает указанную sessionId (или текущую,
|
|
||||||
* если sessionId не задана) и рвёт соответствующее WebSocket-подключение.
|
|
||||||
*/
|
*/
|
||||||
public class Net_CloseActiveSession_Request extends Net_Request {
|
public class Net_CloseActiveSession_Request extends Net_Request {
|
||||||
|
|
||||||
/** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
|
/** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
|
||||||
/** Время на стороне клиента (мс с 1970-01-01). Используется при AUTH_IN_PROGRESS. */
|
|
||||||
private long timeMs;
|
|
||||||
|
|
||||||
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
|
||||||
private String signatureB64;
|
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
@ -41,20 +26,4 @@ public class Net_CloseActiveSession_Request extends Net_Request {
|
|||||||
public void setSessionId(String sessionId) {
|
public void setSessionId(String sessionId) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getTimeMs() {
|
|
||||||
return timeMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimeMs(long timeMs) {
|
|
||||||
this.timeMs = timeMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSignatureB64() {
|
|
||||||
return signatureB64;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignatureB64(String signatureB64) {
|
|
||||||
this.signatureB64 = signatureB64;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -3,40 +3,31 @@ package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
|||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Шаг 2 авторизации: подтверждение владения ключом и установка сессии.
|
* Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
|
||||||
*
|
*
|
||||||
* Клиент:
|
* Шаги:
|
||||||
* 1) получает от сервера authNonce на шаге 1;
|
* 1) AuthChallenge(login) -> authNonce
|
||||||
* 2) генерирует свой StoragePwd (base64 от 32 байт);
|
* 2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
|
||||||
* 3) формирует строку для подписи:
|
|
||||||
* "AUTHORIFICATED:" + timeMs + authNonce
|
|
||||||
* 4) подписывает эту строку своим приватным ключом (pubkey1),
|
|
||||||
* отправляет подпись и StoragePwd на сервер.
|
|
||||||
*
|
*
|
||||||
* Дополнительно:
|
* Подпись deviceKey делается над строкой (UTF-8):
|
||||||
* - clientInfo — короткая строка (до 50 символов) с данными об устройстве/клиенте.
|
* AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
|
||||||
*
|
*
|
||||||
* Формат входящего JSON:
|
* Важно:
|
||||||
* {
|
* - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
|
||||||
* "op": "CreateAuthSession",
|
* - В БД active_sessions.session_key хранится sessionPubKeyB64.
|
||||||
* "requestId": "...",
|
|
||||||
* "payload": {
|
|
||||||
* "storagePwd": "base64-строка-от-32-байт",
|
|
||||||
* "timeMs": 1733310000000,
|
|
||||||
* "signatureB64": "base64-подпись-Ed25519",
|
|
||||||
* "clientInfo": "Chrome/Android" // опционально, до 50 символов
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
*/
|
||||||
public class Net_CreateAuthSession_Request extends Net_Request {
|
public class Net_CreateAuthSession_Request extends Net_Request {
|
||||||
|
|
||||||
/** Клиентский пароль для хранения данных (base64 от 32 байт). */
|
/** Клиентский пароль для хранения данных (base64url от 32 байт). */
|
||||||
private String storagePwd;
|
private String storagePwd;
|
||||||
|
|
||||||
|
/** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
|
||||||
|
private String sessionPubKeyB64;
|
||||||
|
|
||||||
/** Время на стороне клиента (мс с 1970-01-01). */
|
/** Время на стороне клиента (мс с 1970-01-01). */
|
||||||
private long timeMs;
|
private long timeMs;
|
||||||
|
|
||||||
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
/** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
|
||||||
private String signatureB64;
|
private String signatureB64;
|
||||||
|
|
||||||
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||||
@ -50,6 +41,14 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
|||||||
this.storagePwd = storagePwd;
|
this.storagePwd = storagePwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSessionPubKeyB64() {
|
||||||
|
return sessionPubKeyB64;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionPubKeyB64(String sessionPubKeyB64) {
|
||||||
|
this.sessionPubKeyB64 = sessionPubKeyB64;
|
||||||
|
}
|
||||||
|
|
||||||
public long getTimeMs() {
|
public long getTimeMs() {
|
||||||
return timeMs;
|
return timeMs;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,10 @@ package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
|||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ответ на CreateAuthSession.
|
* Ответ на CreateAuthSession (v2).
|
||||||
*
|
*
|
||||||
* При успехе сервер создаёт запись в active_sessions
|
* При успехе сервер создаёт запись в active_sessions
|
||||||
* и возвращает идентификатор сессии sessionId и секрет сессии sessionPwd.
|
* и возвращает идентификатор сессии sessionId.
|
||||||
*
|
*
|
||||||
* JSON:
|
* JSON:
|
||||||
* {
|
* {
|
||||||
@ -14,19 +14,15 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|||||||
* "requestId": "...",
|
* "requestId": "...",
|
||||||
* "status": 200,
|
* "status": 200,
|
||||||
* "payload": {
|
* "payload": {
|
||||||
* "sessionId": "base64-строка-от-32-байт",
|
* "sessionId": "base64url(32)"
|
||||||
* "sessionPwd": "base64-строка-от-32-байт"
|
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public class Net_CreateAuthSession_Response extends Net_Response {
|
public class Net_CreateAuthSession_Response extends Net_Response {
|
||||||
|
|
||||||
/** Идентификатор сессии, base64 от 32 байт. */
|
/** Идентификатор сессии, base64url от 32 байт. */
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
|
||||||
/** Секрет сессии, base64 от 32 байт. */
|
|
||||||
private String sessionPwd;
|
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
@ -34,12 +30,4 @@ public class Net_CreateAuthSession_Response extends Net_Response {
|
|||||||
public void setSessionId(String sessionId) {
|
public void setSessionId(String sessionId) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSessionPwd() {
|
|
||||||
return sessionPwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionPwd(String sessionPwd) {
|
|
||||||
this.sessionPwd = sessionPwd;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -5,50 +5,10 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|||||||
/**
|
/**
|
||||||
* Запрос ListSessions — список активных сессий пользователя.
|
* Запрос ListSessions — список активных сессий пользователя.
|
||||||
*
|
*
|
||||||
* Режимы безопасности такие же, как у CloseActiveSession:
|
* Новая логика (v2):
|
||||||
*
|
* - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
|
||||||
* 1) Пользователь уже авторизован (AUTH_STATUS_USER):
|
* - Пустой payload.
|
||||||
* - поля timeMs и signatureB64 могут быть пустыми и игнорируются.
|
|
||||||
*
|
|
||||||
* 2) Пользователь в статусе AUTH_STATUS_AUTH_IN_PROGRESS:
|
|
||||||
* - требуется подпись Ed25519 над строкой
|
|
||||||
* "AUTHORIFICATED:" + timeMs + authNonce
|
|
||||||
* (authNonce сохранён в ctx.authNonce после AuthChallenge).
|
|
||||||
*
|
|
||||||
* 3) Анонимный клиент (AUTH_STATUS_NONE или нет пользователя в ctx):
|
|
||||||
* - возвращается ошибка NOT_AUTHENTICATED.
|
|
||||||
*
|
|
||||||
* JSON:
|
|
||||||
* {
|
|
||||||
* "op": "ListSessions",
|
|
||||||
* "requestId": "...",
|
|
||||||
* "payload": {
|
|
||||||
* "timeMs": 1733310000000, // при AUTH_IN_PROGRESS
|
|
||||||
* "signatureB64": "base64-подпись" // при AUTH_IN_PROGRESS
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
*/
|
||||||
public class Net_ListSessions_Request extends Net_Request {
|
public class Net_ListSessions_Request extends Net_Request {
|
||||||
|
// пусто
|
||||||
/** Время на стороне клиента (мс с 1970-01-01). Используется при AUTH_IN_PROGRESS. */
|
|
||||||
private long timeMs;
|
|
||||||
|
|
||||||
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
|
||||||
private String signatureB64;
|
|
||||||
|
|
||||||
public long getTimeMs() {
|
|
||||||
return timeMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimeMs(long timeMs) {
|
|
||||||
this.timeMs = timeMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSignatureB64() {
|
|
||||||
return signatureB64;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignatureB64(String signatureB64) {
|
|
||||||
this.signatureB64 = signatureB64;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,48 +1,36 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
//package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||||
|
//
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
//import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* Запрос RefreshSession.
|
// * Запрос RefreshSession.
|
||||||
*
|
// *
|
||||||
* Используется для повторного входа без повторной подписи:
|
// * В новой версии (v2) RefreshSession ОТКЛЮЧЕН.
|
||||||
* клиент хранит sessionId и sessionPwd, которые получил на шаге 2.
|
// * Оставлен временно для совместимости, handler вернёт 410 GONE.
|
||||||
*
|
// */
|
||||||
* JSON (payload):
|
//public class Net_RefreshSession_Request extends Net_Request {
|
||||||
* {
|
//
|
||||||
* "sessionId": "base64-id-сессии",
|
// private String sessionId;
|
||||||
* "sessionPwd": "base64-sessionPwd",
|
// private String sessionPwd;
|
||||||
* "clientInfo": "до 50 символов, краткая строка об устройстве"
|
// private String clientInfo;
|
||||||
* }
|
//
|
||||||
*/
|
// public String getSessionId() {
|
||||||
public class Net_RefreshSession_Request extends Net_Request {
|
// return sessionId;
|
||||||
|
// }
|
||||||
private String sessionId;
|
//
|
||||||
private String sessionPwd;
|
// public void setSessionId(String sessionId) {
|
||||||
|
// this.sessionId = sessionId;
|
||||||
/**
|
// }
|
||||||
* Краткая строка с информацией об устройстве/клиенте, до 50 символов.
|
//
|
||||||
* Например: "PWA/Chrome/Android".
|
// public String getSessionPwd() {
|
||||||
*/
|
// return sessionPwd;
|
||||||
private String clientInfo;
|
// }
|
||||||
|
//
|
||||||
public String getSessionId() {
|
// public void setSessionPwd(String sessionPwd) {
|
||||||
return sessionId;
|
// this.sessionPwd = sessionPwd;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public void setSessionId(String sessionId) {
|
// public String getClientInfo() { return clientInfo; }
|
||||||
this.sessionId = sessionId;
|
//
|
||||||
}
|
// public void setClientInfo(String clientInfo) { this.clientInfo = clientInfo; }
|
||||||
|
//}
|
||||||
public String getSessionPwd() {
|
|
||||||
return sessionPwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionPwd(String sessionPwd) {
|
|
||||||
this.sessionPwd = sessionPwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getClientInfo() { return clientInfo; }
|
|
||||||
|
|
||||||
public void setClientInfo(String clientInfo) { this.clientInfo = clientInfo; }
|
|
||||||
}
|
|
||||||
@ -1,33 +1,23 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
//package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||||
|
//
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
//import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* Успешный ответ на RefreshSession.
|
// * Ответ на RefreshSession.
|
||||||
*
|
// *
|
||||||
* Дополнительно к статусу 200 сервер возвращает storagePwd,
|
// * В новой версии (v2) RefreshSession ОТКЛЮЧЕН.
|
||||||
* чтобы клиент мог восстановить/синхронизировать локальное хранилище.
|
// * Этот класс можно оставить временно для совместимости сериализации,
|
||||||
*
|
// * но handler будет возвращать 410 GONE.
|
||||||
* JSON:
|
// */
|
||||||
* {
|
//public class Net_RefreshSession_Response extends Net_Response {
|
||||||
* "op": "RefreshSession",
|
//
|
||||||
* "requestId": "...",
|
// private String storagePwd;
|
||||||
* "status": 200,
|
//
|
||||||
* "payload": {
|
// public String getStoragePwd() {
|
||||||
* "storagePwd": "base64-строка-от-32-байт"
|
// return storagePwd;
|
||||||
* }
|
// }
|
||||||
* }
|
//
|
||||||
*/
|
// public void setStoragePwd(String storagePwd) {
|
||||||
public class Net_RefreshSession_Response extends Net_Response {
|
// this.storagePwd = storagePwd;
|
||||||
|
// }
|
||||||
/** Пароль хранилища, сохранённый в сессии (storagePwd). */
|
//}
|
||||||
private String storagePwd;
|
|
||||||
|
|
||||||
public String getStoragePwd() {
|
|
||||||
return storagePwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStoragePwd(String storagePwd) {
|
|
||||||
this.storagePwd = storagePwd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Шаг 1 входа в существующую сессию (v2):
|
||||||
|
* SessionChallenge(sessionId) -> nonce
|
||||||
|
*/
|
||||||
|
public class Net_SessionChallenge_Request extends Net_Request {
|
||||||
|
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответ на SessionChallenge (v2).
|
||||||
|
* payload: { "nonce": "base64url(32)" }
|
||||||
|
*/
|
||||||
|
public class Net_SessionChallenge_Response extends Net_Response {
|
||||||
|
|
||||||
|
private String nonce;
|
||||||
|
|
||||||
|
public String getNonce() {
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNonce(String nonce) {
|
||||||
|
this.nonce = nonce;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Шаг 2 входа в существующую сессию (v2):
|
||||||
|
* SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
|
||||||
|
*
|
||||||
|
* Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
|
||||||
|
* SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||||||
|
*
|
||||||
|
* nonce берётся из SessionChallenge и хранится в ctx (одноразовый, TTL).
|
||||||
|
*/
|
||||||
|
public class Net_SessionLogin_Request extends Net_Request {
|
||||||
|
|
||||||
|
private String sessionId;
|
||||||
|
private long timeMs;
|
||||||
|
private String signatureB64;
|
||||||
|
|
||||||
|
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||||
|
private String clientInfo;
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimeMs() {
|
||||||
|
return timeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeMs(long timeMs) {
|
||||||
|
this.timeMs = timeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSignatureB64() {
|
||||||
|
return signatureB64;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignatureB64(String signatureB64) {
|
||||||
|
this.signatureB64 = signatureB64;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientInfo() {
|
||||||
|
return clientInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientInfo(String clientInfo) {
|
||||||
|
this.clientInfo = clientInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответ на SessionLogin (v2).
|
||||||
|
* payload: { "storagePwd": "base64url(32)" }
|
||||||
|
*/
|
||||||
|
public class Net_SessionLogin_Response extends Net_Response {
|
||||||
|
|
||||||
|
private String storagePwd;
|
||||||
|
|
||||||
|
public String getStoragePwd() {
|
||||||
|
return storagePwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStoragePwd(String storagePwd) {
|
||||||
|
this.storagePwd = storagePwd;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,11 +13,16 @@ import java.util.List;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IT_02_Sessions
|
* IT_02_Sessions (v2)
|
||||||
*
|
*
|
||||||
* Цель:
|
* Цель:
|
||||||
* - проверить создание/листинг/refresh/close
|
* - проверить создание/листинг/вход-в-сессию(2 шага)/close
|
||||||
* - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
|
* - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
|
||||||
|
*
|
||||||
|
* Протокол v2:
|
||||||
|
* - создание сессии: AuthChallenge -> CreateAuthSession (deviceKey подпись, + sessionPubKey)
|
||||||
|
* - вход в сессию: SessionChallenge(sessionId) -> nonce, затем SessionLogin(sessionId,time,signature(sessionKey))
|
||||||
|
* - ListSessions и CloseActiveSession доступны только в AUTH_STATUS_USER (после SessionLogin)
|
||||||
*/
|
*/
|
||||||
public class IT_02_Sessions {
|
public class IT_02_Sessions {
|
||||||
|
|
||||||
@ -31,110 +36,65 @@ public class IT_02_Sessions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String run() {
|
public static String run() {
|
||||||
TestResult r = new TestResult("IT_02_Sessions");
|
TestResult r = new TestResult("IT_02_Sessions(v2)");
|
||||||
|
|
||||||
Duration t = Duration.ofSeconds(5);
|
Duration t = Duration.ofSeconds(5);
|
||||||
|
|
||||||
String s1Id, s1Pwd;
|
Session s1, s2, s3;
|
||||||
String s2Id, s2Pwd;
|
|
||||||
String s3Id, s3Pwd;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Создаём 3 сессии (каждая — отдельным соединением, чтобы не зависеть от состояния WS)
|
// 1) Создаём 3 сессии (каждая — отдельным соединением)
|
||||||
Session s1 = createSession(LOGIN, t, r, "S1");
|
s1 = createSession(LOGIN, t, r, "S1");
|
||||||
s1Id = s1.sessionId; s1Pwd = s1.sessionPwd;
|
s2 = createSession(LOGIN, t, r, "S2");
|
||||||
|
s3 = createSession(LOGIN, t, r, "S3");
|
||||||
|
|
||||||
Session s2 = createSession(LOGIN, t, r, "S2");
|
// 2) Входим в S1 (2 шага) и делаем ListSessions (AUTH_STATUS_USER) — должны быть S1,S2,S3
|
||||||
s2Id = s2.sessionId; s2Pwd = s2.sessionPwd;
|
|
||||||
|
|
||||||
Session s3 = createSession(LOGIN, t, r, "S3");
|
|
||||||
s3Id = s3.sessionId; s3Pwd = s3.sessionPwd;
|
|
||||||
|
|
||||||
// 2) ListSessions в AUTH_IN_PROGRESS — должны быть S1,S2,S3
|
|
||||||
try (WsSession ws = WsSession.open()) {
|
try (WsSession ws = WsSession.open()) {
|
||||||
String nonceResp = ws.call("AuthChallenge(list)", JsonBuilders.authChallenge(LOGIN), t);
|
sessionLogin2Steps(ws, s1, t, "Login(S1)", r);
|
||||||
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(list) must be 200");
|
|
||||||
String nonce = JsonParsers.authNonce(nonceResp);
|
|
||||||
assertNotNull(nonce, "authNonce must not be null");
|
|
||||||
|
|
||||||
long timeMs = System.currentTimeMillis();
|
String listResp = ws.call("ListSessions(AUTH_STATUS_USER)", JsonBuilders.listSessions(0L, ""), t);
|
||||||
String sig = JsonBuilders.signAuthorificated(nonce, timeMs, TestConfig.getDevicePrivatKey(LOGIN));
|
assertEquals(200, JsonParsers.status(listResp), "ListSessions(AUTH_STATUS_USER) must be 200");
|
||||||
|
|
||||||
String listResp = ws.call("ListSessions(AUTH_IN_PROGRESS)", JsonBuilders.listSessions(timeMs, sig), t);
|
|
||||||
assertEquals(200, JsonParsers.status(listResp), "ListSessions must be 200");
|
|
||||||
|
|
||||||
List<String> ids = JsonParsers.sessionIds(listResp);
|
List<String> ids = JsonParsers.sessionIds(listResp);
|
||||||
r.ok("ListSessions(AUTH_IN_PROGRESS): " + ids);
|
r.ok("ListSessions(AUTH_STATUS_USER): " + ids);
|
||||||
|
|
||||||
assertTrue(ids.contains(s1Id), "Must contain S1");
|
assertTrue(ids.contains(s1.sessionId), "Must contain S1");
|
||||||
assertTrue(ids.contains(s2Id), "Must contain S2");
|
assertTrue(ids.contains(s2.sessionId), "Must contain S2");
|
||||||
assertTrue(ids.contains(s3Id), "Must contain S3");
|
assertTrue(ids.contains(s3.sessionId), "Must contain S3");
|
||||||
r.ok("Проверка OK: список содержит S1,S2,S3");
|
r.ok("Проверка OK: список содержит S1,S2,S3");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) RefreshSession(S1) -> после refresh в этом же соединении делаем ListSessions(AUTH_STATUS_USER) (timeMs=0)
|
// 3) Проверяем CloseActiveSession так, чтобы итогом всё равно осталось 3 сессии:
|
||||||
try (WsSession ws = WsSession.open()) {
|
// создаём TEMP, логинимся в S1, закрываем TEMP, убеждаемся что S1,S2,S3 остались.
|
||||||
String refreshResp = ws.call("RefreshSession(S1)", JsonBuilders.refreshSession(s1Id, s1Pwd), t);
|
|
||||||
assertEquals(200, JsonParsers.status(refreshResp), "RefreshSession(S1) must be 200");
|
|
||||||
assertNotNull(JsonParsers.storagePwd(refreshResp), "storagePwd must not be null");
|
|
||||||
r.ok("RefreshSession(S1): OK");
|
|
||||||
|
|
||||||
String listInUserResp = ws.call("ListSessions(AUTH_STATUS_USER)", JsonBuilders.listSessions(0L, ""), t);
|
|
||||||
assertEquals(200, JsonParsers.status(listInUserResp), "ListSessions(AUTH_STATUS_USER) must be 200");
|
|
||||||
|
|
||||||
List<String> ids = JsonParsers.sessionIds(listInUserResp);
|
|
||||||
r.ok("ListSessions(AUTH_STATUS_USER): " + ids);
|
|
||||||
|
|
||||||
assertTrue(ids.contains(s1Id));
|
|
||||||
assertTrue(ids.contains(s2Id));
|
|
||||||
assertTrue(ids.contains(s3Id));
|
|
||||||
r.ok("Проверка OK: AUTH_STATUS_USER список содержит S1,S2,S3");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Проверяем CloseActiveSession, но так, чтобы итогом всё равно осталось 3 сессии:
|
|
||||||
// создаём TEMP, закрываем TEMP, убеждаемся что S1,S2,S3 остались.
|
|
||||||
Session temp = createSession(LOGIN, t, r, "TEMP");
|
Session temp = createSession(LOGIN, t, r, "TEMP");
|
||||||
String tempId = temp.sessionId;
|
|
||||||
|
|
||||||
try (WsSession ws = WsSession.open()) {
|
try (WsSession ws = WsSession.open()) {
|
||||||
String nonceResp = ws.call("AuthChallenge(close TEMP)", JsonBuilders.authChallenge(LOGIN), t);
|
sessionLogin2Steps(ws, s1, t, "Login(S1) for close", r);
|
||||||
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(close TEMP) must be 200");
|
|
||||||
String nonce = JsonParsers.authNonce(nonceResp);
|
|
||||||
assertNotNull(nonce);
|
|
||||||
|
|
||||||
long timeMs = System.currentTimeMillis();
|
String closeResp = ws.call("CloseActiveSession(TEMP)", JsonBuilders.closeActiveSession(temp.sessionId, 0L, ""), t);
|
||||||
String sig = JsonBuilders.signAuthorificated(nonce, timeMs, TestConfig.getDevicePrivatKey(LOGIN));
|
|
||||||
|
|
||||||
String closeResp = ws.call("CloseActiveSession(TEMP)", JsonBuilders.closeActiveSession(tempId, timeMs, sig), t);
|
|
||||||
assertEquals(200, JsonParsers.status(closeResp), "CloseActiveSession(TEMP) must be 200");
|
assertEquals(200, JsonParsers.status(closeResp), "CloseActiveSession(TEMP) must be 200");
|
||||||
r.ok("CloseActiveSession(TEMP): OK");
|
r.ok("CloseActiveSession(TEMP): OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Финальная проверка: снова ListSessions(AUTH_IN_PROGRESS) => S1,S2,S3 должны остаться, TEMP нет
|
// 4) Финальная проверка: снова логинимся в S1 и ListSessions => S1,S2,S3 должны остаться, TEMP нет
|
||||||
try (WsSession ws = WsSession.open()) {
|
try (WsSession ws = WsSession.open()) {
|
||||||
String nonceResp = ws.call("AuthChallenge(final list)", JsonBuilders.authChallenge(LOGIN), t);
|
sessionLogin2Steps(ws, s1, t, "Final Login(S1)", r);
|
||||||
assertEquals(200, JsonParsers.status(nonceResp));
|
|
||||||
String nonce = JsonParsers.authNonce(nonceResp);
|
|
||||||
assertNotNull(nonce);
|
|
||||||
|
|
||||||
long timeMs = System.currentTimeMillis();
|
String listResp = ws.call("ListSessions(final)", JsonBuilders.listSessions(0L, ""), t);
|
||||||
String sig = JsonBuilders.signAuthorificated(nonce, timeMs, TestConfig.getDevicePrivatKey(LOGIN));
|
|
||||||
|
|
||||||
String listResp = ws.call("ListSessions(final AUTH_IN_PROGRESS)", JsonBuilders.listSessions(timeMs, sig), t);
|
|
||||||
assertEquals(200, JsonParsers.status(listResp));
|
assertEquals(200, JsonParsers.status(listResp));
|
||||||
|
|
||||||
List<String> ids = JsonParsers.sessionIds(listResp);
|
List<String> ids = JsonParsers.sessionIds(listResp);
|
||||||
r.ok("Final ListSessions: " + ids);
|
r.ok("Final ListSessions: " + ids);
|
||||||
|
|
||||||
assertTrue(ids.contains(s1Id));
|
assertTrue(ids.contains(s1.sessionId));
|
||||||
assertTrue(ids.contains(s2Id));
|
assertTrue(ids.contains(s2.sessionId));
|
||||||
assertTrue(ids.contains(s3Id));
|
assertTrue(ids.contains(s3.sessionId));
|
||||||
assertFalse(ids.contains(tempId));
|
assertFalse(ids.contains(temp.sessionId));
|
||||||
r.ok("ИТОГ OK: после теста в БД остались 3 активные сессии (S1,S2,S3)");
|
r.ok("ИТОГ OK: после теста в БД остались 3 активные сессии (S1,S2,S3)");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
r.fail("IT_02_Sessions упал: " + e.getMessage());
|
r.fail("IT_02_Sessions(v2) упал: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.summaryLine();
|
return r.summaryLine();
|
||||||
@ -142,24 +102,56 @@ public class IT_02_Sessions {
|
|||||||
|
|
||||||
private static Session createSession(String login, Duration t, TestResult r, String label) {
|
private static Session createSession(String login, Duration t, TestResult r, String label) {
|
||||||
try (WsSession ws = WsSession.open()) {
|
try (WsSession ws = WsSession.open()) {
|
||||||
|
|
||||||
|
// шаг 1: AuthChallenge
|
||||||
String nonceResp = ws.call("AuthChallenge(" + label + ")", JsonBuilders.authChallenge(login), t);
|
String nonceResp = ws.call("AuthChallenge(" + label + ")", JsonBuilders.authChallenge(login), t);
|
||||||
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(" + label + ") must be 200");
|
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(" + label + ") must be 200");
|
||||||
String nonce = JsonParsers.authNonce(nonceResp);
|
String authNonce = JsonParsers.authNonce(nonceResp);
|
||||||
assertNotNull(nonce, "authNonce must not be null for " + label);
|
assertNotNull(authNonce, "authNonce must not be null for " + label);
|
||||||
|
|
||||||
String createResp = ws.call("CreateAuthSession(" + label + ")", JsonBuilders.createAuthSession(login, nonce, TestConfig.fakeStoragePwd()), t);
|
// для тестов: sessionKey = deviceKey (в реале будет отдельный keypair)
|
||||||
|
String sessionPubKeyB64 = TestConfig.devicePublicKeyB64(login);
|
||||||
|
|
||||||
|
// storagePwd на клиенте (сохраняем, чтобы потом проверить, что сервер вернул именно его)
|
||||||
|
String storagePwd = TestConfig.fakeStoragePwd();
|
||||||
|
|
||||||
|
// шаг 2: CreateAuthSession (device подпись + sessionPubKey)
|
||||||
|
String createResp = ws.call(
|
||||||
|
"CreateAuthSession(" + label + ")",
|
||||||
|
JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionPubKeyB64),
|
||||||
|
t
|
||||||
|
);
|
||||||
assertEquals(200, JsonParsers.status(createResp), "CreateAuthSession(" + label + ") must be 200");
|
assertEquals(200, JsonParsers.status(createResp), "CreateAuthSession(" + label + ") must be 200");
|
||||||
|
|
||||||
String sid = JsonParsers.sessionId(createResp);
|
String sid = JsonParsers.sessionId(createResp);
|
||||||
String spw = JsonParsers.sessionPwd(createResp);
|
|
||||||
|
|
||||||
assertNotNull(sid, "sessionId must not be null");
|
assertNotNull(sid, "sessionId must not be null");
|
||||||
assertNotNull(spw, "sessionPwd must not be null");
|
|
||||||
|
|
||||||
r.ok("Создана сессия " + label + ": sessionId=" + sid);
|
r.ok("Создана сессия " + label + ": sessionId=" + sid);
|
||||||
return new Session(sid, spw);
|
|
||||||
|
// для тестов используем devicePriv как sessionPriv
|
||||||
|
byte[] sessionPrivKey = TestConfig.getDevicePrivatKey(login);
|
||||||
|
|
||||||
|
return new Session(sid, sessionPrivKey, storagePwd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Session(String sessionId, String sessionPwd) {}
|
private static void sessionLogin2Steps(WsSession ws, Session s, Duration t, String label, TestResult r) {
|
||||||
|
// шаг 1: SessionChallenge(sessionId)
|
||||||
|
String chResp = ws.call("SessionChallenge " + label, JsonBuilders.sessionChallenge(s.sessionId), t);
|
||||||
|
assertEquals(200, JsonParsers.status(chResp), "SessionChallenge must be 200");
|
||||||
|
String nonce = JsonParsers.sessionNonce(chResp);
|
||||||
|
assertNotNull(nonce, "SessionChallenge nonce must not be null");
|
||||||
|
|
||||||
|
// шаг 2: SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
|
||||||
|
String loginResp = ws.call("SessionLogin " + label, JsonBuilders.sessionLogin(s.sessionId, nonce, s.sessionPrivKey), t);
|
||||||
|
assertEquals(200, JsonParsers.status(loginResp), "SessionLogin must be 200");
|
||||||
|
|
||||||
|
String storagePwd = JsonParsers.storagePwd(loginResp);
|
||||||
|
assertNotNull(storagePwd, "storagePwd must not be null after SessionLogin");
|
||||||
|
assertEquals(s.storagePwd, storagePwd, "storagePwd must match what client provided on CreateAuthSession");
|
||||||
|
|
||||||
|
r.ok(label + ": SessionLogin OK, storagePwd verified");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Session(String sessionId, byte[] sessionPrivKey, String storagePwd) {}
|
||||||
}
|
}
|
||||||
@ -4,7 +4,6 @@ import test.it.cases.IT_01_AddUser;
|
|||||||
import test.it.cases.IT_02_Sessions;
|
import test.it.cases.IT_02_Sessions;
|
||||||
import test.it.cases.IT_03_AddBlock_NoAuth;
|
import test.it.cases.IT_03_AddBlock_NoAuth;
|
||||||
import test.it.cases.IT_04_UserParams_NoAuth;
|
import test.it.cases.IT_04_UserParams_NoAuth;
|
||||||
import test.it.cases.IT_05_ListSubscribedChannels_200;
|
|
||||||
import test.it.utils.log.TestLog;
|
import test.it.utils.log.TestLog;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -31,7 +30,6 @@ public class IT_RunAllMain {
|
|||||||
String s2 = IT_02_Sessions.run(); summaries.add(s2); if (s2.contains("FAIL:")) failed++;
|
String s2 = IT_02_Sessions.run(); summaries.add(s2); if (s2.contains("FAIL:")) failed++;
|
||||||
String s3 = IT_03_AddBlock_NoAuth.run(); summaries.add(s3); if (s3.contains("FAIL:")) failed++;
|
String s3 = IT_03_AddBlock_NoAuth.run(); summaries.add(s3); if (s3.contains("FAIL:")) failed++;
|
||||||
String s4 = IT_04_UserParams_NoAuth.run(); summaries.add(s4); if (s4.contains("FAIL:")) failed++;
|
String s4 = IT_04_UserParams_NoAuth.run(); summaries.add(s4); if (s4.contains("FAIL:")) failed++;
|
||||||
String s5 = IT_05_ListSubscribedChannels_200.run(); summaries.add(s5); if (s5.contains("FAIL:")) failed++;
|
|
||||||
|
|
||||||
TestLog.title("IT RUN RESULT (per test)");
|
TestLog.title("IT RUN RESULT (per test)");
|
||||||
for (String s : summaries) System.out.println(s);
|
for (String s : summaries) System.out.println(s);
|
||||||
|
|||||||
@ -58,9 +58,11 @@ public final class JsonBuilders {
|
|||||||
""".formatted(requestId, login);
|
""".formatted(requestId, login);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- CreateAuthSession ----------------
|
// ---------------- CreateAuthSession (v2) ----------------
|
||||||
|
// v2: sessionKey генерируется на клиенте, на сервер отправляем только sessionPubKey (base64).
|
||||||
|
// подпись шага CreateAuthSession всё ещё делается deviceKey: "AUTHORIFICATED:" + timeMs + authNonce
|
||||||
|
|
||||||
public static String createAuthSession(String login, String authNonce, String storagePwd) {
|
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionPubKeyB64) {
|
||||||
long timeMs = System.currentTimeMillis();
|
long timeMs = System.currentTimeMillis();
|
||||||
byte[] devicePriv = TestConfig.getDevicePrivatKey(login);
|
byte[] devicePriv = TestConfig.getDevicePrivatKey(login);
|
||||||
String sigB64 = signAuthorificated(authNonce, timeMs, devicePriv);
|
String sigB64 = signAuthorificated(authNonce, timeMs, devicePriv);
|
||||||
@ -72,12 +74,56 @@ public final class JsonBuilders {
|
|||||||
"requestId": "%s",
|
"requestId": "%s",
|
||||||
"payload": {
|
"payload": {
|
||||||
"storagePwd": "%s",
|
"storagePwd": "%s",
|
||||||
|
"sessionPubKeyB64": "%s",
|
||||||
"timeMs": %d,
|
"timeMs": %d,
|
||||||
"signatureB64": "%s",
|
"signatureB64": "%s",
|
||||||
"clientInfo": "%s"
|
"clientInfo": "%s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(requestId, storagePwd, timeMs, sigB64, TestConfig.TEST_CLIENT_INFO);
|
""".formatted(
|
||||||
|
requestId,
|
||||||
|
storagePwd,
|
||||||
|
sessionPubKeyB64,
|
||||||
|
timeMs,
|
||||||
|
sigB64,
|
||||||
|
TestConfig.TEST_CLIENT_INFO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- SessionChallenge (v2) ----------------
|
||||||
|
|
||||||
|
public static String sessionChallenge(String sessionId) {
|
||||||
|
String requestId = TestIds.next("sch");
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"op": "SessionChallenge",
|
||||||
|
"requestId": "%s",
|
||||||
|
"payload": {
|
||||||
|
"sessionId": "%s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(requestId, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- SessionLogin (v2) ----------------
|
||||||
|
|
||||||
|
public static String sessionLogin(String sessionId, String nonce, byte[] sessionPrivKey) {
|
||||||
|
long timeMs = System.currentTimeMillis();
|
||||||
|
String sigB64 = signSessionLogin(sessionId, timeMs, nonce, sessionPrivKey);
|
||||||
|
|
||||||
|
String requestId = TestIds.next("slogin");
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"op": "SessionLogin",
|
||||||
|
"requestId": "%s",
|
||||||
|
"payload": {
|
||||||
|
"sessionId": "%s",
|
||||||
|
"timeMs": %d,
|
||||||
|
"signatureB64": "%s",
|
||||||
|
"clientInfo": "%s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(requestId, sessionId, timeMs, sigB64, TestConfig.TEST_CLIENT_INFO);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- ListSessions ----------------
|
// ---------------- ListSessions ----------------
|
||||||
@ -97,23 +143,6 @@ public final class JsonBuilders {
|
|||||||
""".formatted(requestId, timeMs, signatureB64);
|
""".formatted(requestId, timeMs, signatureB64);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- RefreshSession ----------------
|
|
||||||
|
|
||||||
public static String refreshSession(String sessionId, String sessionPwd) {
|
|
||||||
String requestId = TestIds.next("refresh");
|
|
||||||
return """
|
|
||||||
{
|
|
||||||
"op": "RefreshSession",
|
|
||||||
"requestId": "%s",
|
|
||||||
"payload": {
|
|
||||||
"sessionId": "%s",
|
|
||||||
"sessionPwd": "%s",
|
|
||||||
"clientInfo": "%s"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".formatted(requestId, sessionId, sessionPwd, TestConfig.TEST_CLIENT_INFO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- CloseActiveSession ----------------
|
// ---------------- CloseActiveSession ----------------
|
||||||
|
|
||||||
public static String closeActiveSession(String sessionId, long timeMs, String signatureB64) {
|
public static String closeActiveSession(String sessionId, long timeMs, String signatureB64) {
|
||||||
@ -145,7 +174,6 @@ public final class JsonBuilders {
|
|||||||
""".formatted(requestId, login);
|
""".formatted(requestId, login);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Подпись для режима AUTH_IN_PROGRESS:
|
* Подпись для режима AUTH_IN_PROGRESS:
|
||||||
* preimage = "AUTHORIFICATED:" + timeMs + authNonce
|
* preimage = "AUTHORIFICATED:" + timeMs + authNonce
|
||||||
@ -157,4 +185,16 @@ public final class JsonBuilders {
|
|||||||
byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
|
byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
|
||||||
return Base64.getEncoder().encodeToString(sig);
|
return Base64.getEncoder().encodeToString(sig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подпись для SessionLogin(v2):
|
||||||
|
* preimage = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce
|
||||||
|
* подписываем sessionPrivKey.
|
||||||
|
*/
|
||||||
|
public static String signSessionLogin(String sessionId, long timeMs, String nonce, byte[] sessionPrivKey) {
|
||||||
|
String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
|
||||||
|
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] sig = Ed25519Util.sign(preimage, sessionPrivKey);
|
||||||
|
return Base64.getEncoder().encodeToString(sig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -30,6 +30,18 @@ public final class JsonParsers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** nonce из SessionChallenge(v2) */
|
||||||
|
public static String sessionNonce(String json) {
|
||||||
|
try {
|
||||||
|
JsonNode root = MAPPER.readTree(json);
|
||||||
|
JsonNode payload = root.get("payload");
|
||||||
|
if (payload != null && payload.has("nonce")) return payload.get("nonce").asText();
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String sessionId(String json) {
|
public static String sessionId(String json) {
|
||||||
try {
|
try {
|
||||||
JsonNode root = MAPPER.readTree(json);
|
JsonNode root = MAPPER.readTree(json);
|
||||||
@ -41,6 +53,7 @@ public final class JsonParsers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// оставляю для совместимости с другими тестами, но в IT_02(v2) больше не используется
|
||||||
public static String sessionPwd(String json) {
|
public static String sessionPwd(String json) {
|
||||||
try {
|
try {
|
||||||
JsonNode root = MAPPER.readTree(json);
|
JsonNode root = MAPPER.readTree(json);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user