fix: enhance authorization handling with improved logging for missing and invalid headers

This commit is contained in:
2025-10-27 18:27:50 +01:00
parent f48b9585ac
commit 7e77cbac32
3 changed files with 35 additions and 6 deletions
@@ -4,6 +4,7 @@ import android.content.Context
import com.collabtable.app.data.preferences.PreferencesManager import com.collabtable.app.data.preferences.PreferencesManager
import android.os.Build import android.os.Build
import android.net.Uri import android.net.Uri
import com.collabtable.app.utils.Logger
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
@@ -32,7 +33,7 @@ object ApiClient {
val newRequest = val newRequest =
if (!password.isNullOrBlank()) { if (!password.isNullOrBlank()) {
request.newBuilder() request.newBuilder()
.header("Authorization", "Bearer $password") .header("Authorization", "Bearer ${'$'}password")
.build() .build()
} else { } else {
request request
@@ -101,11 +102,17 @@ object ApiClient {
this.context = context.applicationContext this.context = context.applicationContext
val prefs = PreferencesManager.getInstance(context) val prefs = PreferencesManager.getInstance(context)
baseUrl = normalizeForAndroidEmulator(prefs.getServerUrl()) baseUrl = normalizeForAndroidEmulator(prefs.getServerUrl())
try {
Logger.i("HTTP", "API base URL initialized to $baseUrl")
} catch (_: Exception) { }
retrofit = buildRetrofit() retrofit = buildRetrofit()
} }
fun setBaseUrl(url: String) { fun setBaseUrl(url: String) {
baseUrl = normalizeForAndroidEmulator(url) baseUrl = normalizeForAndroidEmulator(url)
try {
Logger.i("HTTP", "API base URL set to $baseUrl")
} catch (_: Exception) { }
retrofit = buildRetrofit() retrofit = buildRetrofit()
} }
@@ -41,7 +41,7 @@ class ServerSetupViewModel(
fun validateAndSaveServerUrl( fun validateAndSaveServerUrl(
url: String, url: String,
password: String, password: String,
) { ) {
viewModelScope.launch { viewModelScope.launch {
_isValidating.value = true _isValidating.value = true
@@ -85,8 +85,9 @@ class ServerSetupViewModel(
// Ensure it ends with / // Ensure it ends with /
normalizedUrl = if (normalizedUrl.endsWith("/")) normalizedUrl else "$normalizedUrl/" normalizedUrl = if (normalizedUrl.endsWith("/")) normalizedUrl else "$normalizedUrl/"
// Validate password is not empty // Validate password is not empty (trim leading/trailing spaces)
if (password.isBlank()) { val trimmedPassword = password.trim()
if (trimmedPassword.isBlank()) {
_validationError.value = "Password cannot be empty" _validationError.value = "Password cannot be empty"
_isValidating.value = false _isValidating.value = false
return@launch return@launch
@@ -178,7 +179,7 @@ class ServerSetupViewModel(
val authRequest = val authRequest =
Request.Builder() Request.Builder()
.url(testUrl) .url(testUrl)
.header("Authorization", "Bearer $password") .header("Authorization", "Bearer ${'$'}trimmedPassword")
.get() .get()
.build() .build()
@@ -191,7 +192,7 @@ class ServerSetupViewModel(
if (authResponse.isSuccessful) { if (authResponse.isSuccessful) {
// Password is valid, save both URL and password // Password is valid, save both URL and password
preferencesManager.setServerUrl(finalUrl) preferencesManager.setServerUrl(finalUrl)
preferencesManager.setServerPassword(password) preferencesManager.setServerPassword(trimmedPassword)
// Reset sync baseline for a fresh initial sync on new server // Reset sync baseline for a fresh initial sync on new server
preferencesManager.clearSyncState() preferencesManager.clearSyncState()
preferencesManager.setIsFirstRun(false) preferencesManager.setIsFirstRun(false)
+21
View File
@@ -18,6 +18,12 @@ export const authenticatePassword = (req: Request, res: Response, next: NextFunc
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader) { if (!authHeader) {
// Log diagnostic info to help trace intermittent 401s
console.warn('[AUTH] Missing Authorization header', {
path: req.path,
method: req.method,
ip: req.ip,
});
return res.status(401).json({ return res.status(401).json({
error: 'Unauthorized', error: 'Unauthorized',
message: 'No authorization header provided' message: 'No authorization header provided'
@@ -27,6 +33,13 @@ export const authenticatePassword = (req: Request, res: Response, next: NextFunc
// Parse the authorization header // Parse the authorization header
const parts = authHeader.split(' '); const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') { if (parts.length !== 2 || parts[0] !== 'Bearer') {
console.warn('[AUTH] Invalid authorization header format', {
path: req.path,
method: req.method,
ip: req.ip,
// Do not log full header to avoid leaking secrets; include prefix only
headerPreview: authHeader.substring(0, Math.min(authHeader.length, 20)) + '...'
});
return res.status(401).json({ return res.status(401).json({
error: 'Unauthorized', error: 'Unauthorized',
message: 'Invalid authorization header format. Expected: Bearer <password>' message: 'Invalid authorization header format. Expected: Bearer <password>'
@@ -37,6 +50,14 @@ export const authenticatePassword = (req: Request, res: Response, next: NextFunc
// Validate password // Validate password
if (providedPassword !== serverPassword) { if (providedPassword !== serverPassword) {
// Avoid logging the full password; log only length and a small preview
const safePreview = providedPassword ? `${providedPassword.substring(0, 2)}…(${providedPassword.length})` : 'null';
console.warn('[AUTH] Invalid password', {
path: req.path,
method: req.method,
ip: req.ip,
providedPreview: safePreview,
});
return res.status(401).json({ return res.status(401).json({
error: 'Unauthorized', error: 'Unauthorized',
message: 'Invalid password' message: 'Invalid password'