mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Android + web player: handle device_token authentication
Follows up on the security audit remediation (afbe113) which added
device_token auth to the WebSocket /device namespace.
Android player (ServerConfig.kt, WebSocketService.kt):
- Persist device_token in EncryptedSharedPreferences alongside device_id
- Send device_token in device:register on reconnect and playlist refresh
- Save/overwrite token from device:registered response (handles legacy
devices getting their first token)
- Handle device:auth-error by clearing credentials and showing pairing screen
- clearDeviceCredentials() method wipes device_id, device_token, is_paired
Web player (player/index.html):
- Save deviceToken in localStorage config from device:registered response
- Send device_token in register() payload on reconnect
- Handle device:auth-error and device:unpaired events — clear config and
show re-pair UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
afbe113acf
commit
1d253c4cae
|
|
@ -33,6 +33,10 @@ class ServerConfig(context: Context) {
|
||||||
get() = prefs.getString("device_id", "") ?: ""
|
get() = prefs.getString("device_id", "") ?: ""
|
||||||
set(value) = prefs.edit().putString("device_id", value).apply()
|
set(value) = prefs.edit().putString("device_id", value).apply()
|
||||||
|
|
||||||
|
var deviceToken: String
|
||||||
|
get() = prefs.getString("device_token", "") ?: ""
|
||||||
|
set(value) = prefs.edit().putString("device_token", value).apply()
|
||||||
|
|
||||||
var deviceName: String
|
var deviceName: String
|
||||||
get() = prefs.getString("device_name", "Unnamed Display") ?: "Unnamed Display"
|
get() = prefs.getString("device_name", "Unnamed Display") ?: "Unnamed Display"
|
||||||
set(value) = prefs.edit().putString("device_name", value).apply()
|
set(value) = prefs.edit().putString("device_name", value).apply()
|
||||||
|
|
@ -47,6 +51,14 @@ class ServerConfig(context: Context) {
|
||||||
prefs.edit().putBoolean("is_paired", paired).apply()
|
prefs.edit().putBoolean("is_paired", paired).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearDeviceCredentials() {
|
||||||
|
prefs.edit()
|
||||||
|
.remove("device_id")
|
||||||
|
.remove("device_token")
|
||||||
|
.remove("is_paired")
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
prefs.edit().clear().apply()
|
prefs.edit().clear().apply()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,15 +102,25 @@ class WebSocketService : Service() {
|
||||||
val data = args[0] as JSONObject
|
val data = args[0] as JSONObject
|
||||||
val newDeviceId = data.getString("device_id")
|
val newDeviceId = data.getString("device_id")
|
||||||
config.deviceId = newDeviceId
|
config.deviceId = newDeviceId
|
||||||
|
// Persist device_token (issued on first register, or refreshed on reconnect)
|
||||||
|
if (data.has("device_token")) {
|
||||||
|
config.deviceToken = data.getString("device_token")
|
||||||
|
}
|
||||||
Log.i("WebSocketService", "Registered as: $newDeviceId")
|
Log.i("WebSocketService", "Registered as: $newDeviceId")
|
||||||
handler.post { onRegistered?.invoke(newDeviceId) }
|
handler.post { onRegistered?.invoke(newDeviceId) }
|
||||||
startHeartbeat()
|
startHeartbeat()
|
||||||
}
|
}
|
||||||
|
|
||||||
on("device:unpaired") {
|
on("device:unpaired") {
|
||||||
Log.w("WebSocketService", "Device not found on server - clearing config")
|
Log.w("WebSocketService", "Device not found on server - clearing credentials")
|
||||||
config.setPaired(false)
|
config.clearDeviceCredentials()
|
||||||
config.deviceId = ""
|
handler.post { onUnpaired?.invoke() }
|
||||||
|
}
|
||||||
|
|
||||||
|
on("device:auth-error") { args ->
|
||||||
|
val msg = (args.firstOrNull() as? JSONObject)?.optString("error", "Authentication failed") ?: "Authentication failed"
|
||||||
|
Log.w("WebSocketService", "Device auth rejected: $msg — clearing credentials for re-pair")
|
||||||
|
config.clearDeviceCredentials()
|
||||||
handler.post { onUnpaired?.invoke() }
|
handler.post { onUnpaired?.invoke() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,6 +244,11 @@ class WebSocketService : Service() {
|
||||||
val data = JSONObject().apply {
|
val data = JSONObject().apply {
|
||||||
if (config.isProvisioned && config.isPaired) {
|
if (config.isProvisioned && config.isPaired) {
|
||||||
put("device_id", config.deviceId)
|
put("device_id", config.deviceId)
|
||||||
|
// Send device_token for authentication (may be empty for legacy devices)
|
||||||
|
val token = config.deviceToken
|
||||||
|
if (token.isNotEmpty()) {
|
||||||
|
put("device_token", token)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Generate a pairing code if we don't have one
|
// Generate a pairing code if we don't have one
|
||||||
val pairingCode = (100000..999999).random().toString()
|
val pairingCode = (100000..999999).random().toString()
|
||||||
|
|
@ -279,6 +294,10 @@ class WebSocketService : Service() {
|
||||||
// Re-register triggers the server to send current playlist
|
// Re-register triggers the server to send current playlist
|
||||||
val data = org.json.JSONObject().apply {
|
val data = org.json.JSONObject().apply {
|
||||||
put("device_id", config.deviceId)
|
put("device_id", config.deviceId)
|
||||||
|
val token = config.deviceToken
|
||||||
|
if (token.isNotEmpty()) {
|
||||||
|
put("device_token", token)
|
||||||
|
}
|
||||||
put("device_info", deviceInfo.getDeviceInfo())
|
put("device_info", deviceInfo.getDeviceInfo())
|
||||||
}
|
}
|
||||||
socket?.emit("device:register", data)
|
socket?.emit("device:register", data)
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,7 @@
|
||||||
|
|
||||||
socket.on('device:registered', (data) => {
|
socket.on('device:registered', (data) => {
|
||||||
config.deviceId = data.device_id;
|
config.deviceId = data.device_id;
|
||||||
|
if (data.device_token) config.deviceToken = data.device_token;
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
console.log('Registered:', data.device_id);
|
console.log('Registered:', data.device_id);
|
||||||
|
|
||||||
|
|
@ -278,6 +279,30 @@
|
||||||
showStatus('Waiting for content...');
|
showStatus('Waiting for content...');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('device:unpaired', () => {
|
||||||
|
console.warn('Device not found on server — clearing credentials');
|
||||||
|
delete config.deviceId;
|
||||||
|
delete config.deviceToken;
|
||||||
|
config.paired = false;
|
||||||
|
saveConfig(config);
|
||||||
|
document.getElementById('setupScreen').style.display = 'flex';
|
||||||
|
document.getElementById('urlForm').style.display = 'block';
|
||||||
|
document.getElementById('pairingSection').style.display = 'none';
|
||||||
|
document.getElementById('setupStatus').textContent = 'Device was removed from server. Please reconnect.';
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('device:auth-error', (data) => {
|
||||||
|
console.warn('Device auth rejected:', data?.error || 'unknown');
|
||||||
|
delete config.deviceId;
|
||||||
|
delete config.deviceToken;
|
||||||
|
config.paired = false;
|
||||||
|
saveConfig(config);
|
||||||
|
document.getElementById('setupScreen').style.display = 'flex';
|
||||||
|
document.getElementById('urlForm').style.display = 'block';
|
||||||
|
document.getElementById('pairingSection').style.display = 'none';
|
||||||
|
document.getElementById('setupStatus').textContent = 'Authentication failed. Please re-pair this device.';
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('device:playlist-update', (data) => {
|
socket.on('device:playlist-update', (data) => {
|
||||||
console.log('Playlist update:', data.assignments?.length, 'items');
|
console.log('Playlist update:', data.assignments?.length, 'items');
|
||||||
handlePlaylistUpdate(data);
|
handlePlaylistUpdate(data);
|
||||||
|
|
@ -361,6 +386,7 @@
|
||||||
const data = {};
|
const data = {};
|
||||||
if (config.deviceId && config.paired) {
|
if (config.deviceId && config.paired) {
|
||||||
data.device_id = config.deviceId;
|
data.device_id = config.deviceId;
|
||||||
|
if (config.deviceToken) data.device_token = config.deviceToken;
|
||||||
} else {
|
} else {
|
||||||
const code = String(Math.floor(100000 + Math.random() * 900000));
|
const code = String(Math.floor(100000 + Math.random() * 900000));
|
||||||
config.pairingCode = code;
|
config.pairingCode = code;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue