mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Endurain: Support refreshing auth token
This commit is contained in:
+36
-39
@@ -22,11 +22,17 @@ import com.google.gson.Gson
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
enum class AuthType {
|
||||
NONE,
|
||||
AUTH_TOKEN,
|
||||
REFRESH_TOKEN
|
||||
}
|
||||
|
||||
data class LoginResponse(
|
||||
val session_id: String? = null,
|
||||
val access_token: String? = null,
|
||||
val refresh_token: String? = null,
|
||||
val expires_in: Long? = null,
|
||||
val expires_in: Int? = null,
|
||||
val token_type: String? = null,
|
||||
val mfa_required: Boolean? = null,
|
||||
val username: String? = null,
|
||||
@@ -38,10 +44,6 @@ data class MfaVerifyRequest(
|
||||
val mfa_code: String
|
||||
)
|
||||
|
||||
data class TokenRefreshRequest(
|
||||
val refresh_token: String
|
||||
)
|
||||
|
||||
data class TokenExchangeRequest(
|
||||
val code_verifier: String
|
||||
)
|
||||
@@ -56,12 +58,22 @@ class EndurainApiClient(
|
||||
/**
|
||||
* Build headers with authentication tokens
|
||||
*/
|
||||
private fun buildHeaders(): Map<String, String> {
|
||||
private fun buildHeaders(auth: AuthType): MutableMap<String, String> {
|
||||
val headers = mutableMapOf("X-Client-Type" to "mobile")
|
||||
|
||||
when (auth) {
|
||||
AuthType.AUTH_TOKEN -> {
|
||||
tokenManager.getAccessToken()?.let { token ->
|
||||
headers["Authorization"] = "Bearer $token"
|
||||
}
|
||||
}
|
||||
AuthType.REFRESH_TOKEN -> {
|
||||
tokenManager.getRefreshToken()?.let { token ->
|
||||
headers["Authorization"] = "Bearer $token"
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
@@ -76,15 +88,14 @@ class EndurainApiClient(
|
||||
// Form-encoded body
|
||||
val body = "username=${Uri.encode(username)}&password=${Uri.encode(password)}"
|
||||
|
||||
val headers = mapOf("X-Client-Type" to "mobile")
|
||||
val headers = buildHeaders(AuthType.NONE)
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
|
||||
val responseText = InternetUtils.doStringRequest(
|
||||
uri = uri,
|
||||
method = "POST",
|
||||
requestHeaders = headers,
|
||||
body = body,
|
||||
bodyContentType = "application/x-www-form-urlencoded",
|
||||
allowInsecure = false
|
||||
body = body
|
||||
)
|
||||
|
||||
return if (responseText != null) {
|
||||
@@ -109,15 +120,14 @@ class EndurainApiClient(
|
||||
val request = MfaVerifyRequest(username, mfaCode)
|
||||
val body = gson.toJson(request)
|
||||
|
||||
val headers = mapOf("X-Client-Type" to "mobile")
|
||||
val headers = buildHeaders(AuthType.NONE)
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
val responseText = InternetUtils.doStringRequest(
|
||||
uri = uri,
|
||||
method = "POST",
|
||||
requestHeaders = headers,
|
||||
body = body,
|
||||
bodyContentType = "application/json",
|
||||
allowInsecure = false
|
||||
body = body
|
||||
)
|
||||
|
||||
return if (responseText != null) {
|
||||
@@ -137,26 +147,16 @@ class EndurainApiClient(
|
||||
*/
|
||||
fun refreshToken(): LoginResponse? {
|
||||
try {
|
||||
val refreshToken = tokenManager.getRefreshToken()
|
||||
if (refreshToken == null) {
|
||||
LOG.error("No refresh token available")
|
||||
return null
|
||||
}
|
||||
|
||||
val uri = "$baseUrl/api/v1/auth/refresh".toUri()
|
||||
|
||||
val request = TokenRefreshRequest(refreshToken)
|
||||
val body = gson.toJson(request)
|
||||
|
||||
val headers = buildHeaders()
|
||||
val headers = buildHeaders(AuthType.REFRESH_TOKEN)
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
val responseText = InternetUtils.doStringRequest(
|
||||
uri = uri,
|
||||
method = "POST",
|
||||
requestHeaders = headers,
|
||||
body = body,
|
||||
bodyContentType = "application/json",
|
||||
allowInsecure = false
|
||||
body = "{}"
|
||||
)
|
||||
|
||||
return if (responseText != null) {
|
||||
@@ -178,15 +178,14 @@ class EndurainApiClient(
|
||||
try {
|
||||
val uri = "$baseUrl/api/v1/auth/logout".toUri()
|
||||
|
||||
val headers = buildHeaders()
|
||||
val headers = buildHeaders(AuthType.AUTH_TOKEN)
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
InternetUtils.doStringRequest(
|
||||
uri = uri,
|
||||
method = "POST",
|
||||
requestHeaders = headers,
|
||||
body = null,
|
||||
bodyContentType = "application/json",
|
||||
allowInsecure = false
|
||||
body = "{}"
|
||||
)
|
||||
|
||||
tokenManager.clearTokens()
|
||||
@@ -207,15 +206,14 @@ class EndurainApiClient(
|
||||
val request = TokenExchangeRequest(codeVerifier)
|
||||
val body = gson.toJson(request)
|
||||
|
||||
val headers = mapOf("X-Client-Type" to "mobile")
|
||||
val headers = buildHeaders(AuthType.NONE)
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
val responseText = InternetUtils.doStringRequest(
|
||||
uri = uri,
|
||||
method = "POST",
|
||||
requestHeaders = headers,
|
||||
body = body,
|
||||
bodyContentType = "application/json",
|
||||
allowInsecure = false
|
||||
body = body
|
||||
)
|
||||
|
||||
return if (responseText != null) {
|
||||
@@ -241,15 +239,14 @@ class EndurainApiClient(
|
||||
try {
|
||||
val uri = "$baseUrl$endpoint".toUri()
|
||||
|
||||
val headers = buildHeaders()
|
||||
val headers = buildHeaders(AuthType.AUTH_TOKEN)
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
return InternetUtils.doStringRequest(
|
||||
uri = uri,
|
||||
method = method,
|
||||
requestHeaders = headers,
|
||||
body = body,
|
||||
bodyContentType = "application/json",
|
||||
allowInsecure = false
|
||||
body = body
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Authenticated request error", e)
|
||||
|
||||
+15
-1
@@ -42,7 +42,20 @@ class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
|
||||
wireLoginPreference()
|
||||
wireLogoutPreference()
|
||||
updateStatus()
|
||||
updateLogoutPreferenceVisibility()
|
||||
setupLoginResultListener()
|
||||
|
||||
// Refresh auth token
|
||||
val vm: EndurainSetupViewModel by viewModels()
|
||||
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
||||
if (server != null) {
|
||||
vm.performTokenRefresh(server) {
|
||||
activity?.runOnUiThread {
|
||||
updateStatus()
|
||||
updateLogoutPreferenceVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupLoginResultListener() {
|
||||
@@ -106,9 +119,10 @@ class EndurainPreferencesActivity : AbstractSettingsActivityV2() {
|
||||
private fun updateStatus() {
|
||||
val statusPref = findPreference<Preference>("pref_key_status")
|
||||
val server = GBApplication.getPrefs().preferences.getString("endurain_server", null)
|
||||
val tokenExpiresAt = vm.getTokenExpiresAt()
|
||||
|
||||
if (vm.isLoggedIn() && server != null) {
|
||||
statusPref?.summary = "Logged in to $server"
|
||||
statusPref?.summary = "Logged in to $server\nAuth token expires: $tokenExpiresAt"
|
||||
} else {
|
||||
statusPref?.summary = "Not logged in, integration is disabled"
|
||||
}
|
||||
|
||||
+52
-7
@@ -19,8 +19,10 @@ package nodomain.freeyourgadget.gadgetbridge.activities.endurain
|
||||
import android.app.Application
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.InternetUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.Date
|
||||
|
||||
class EndurainSetupViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
@@ -50,11 +52,7 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
||||
try {
|
||||
// Fetch server settings from the public endpoint
|
||||
val settingsUri = "$serverUrl/api/v1/public/server_settings".toUri()
|
||||
val settingsResponse = InternetUtils.doJsonRequest(
|
||||
uri = settingsUri,
|
||||
method = "GET",
|
||||
allowInsecure = false
|
||||
)
|
||||
val settingsResponse = InternetUtils.doJsonRequest(settingsUri)
|
||||
|
||||
if (settingsResponse != null) {
|
||||
// Parse server settings to determine available authentication methods
|
||||
@@ -87,6 +85,44 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
||||
}.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform token refresh
|
||||
*/
|
||||
fun performTokenRefresh(
|
||||
serverUrl: String,
|
||||
callback: (Boolean) -> Unit
|
||||
) {
|
||||
Thread {
|
||||
try {
|
||||
apiClient = EndurainApiClient(serverUrl, tokenManager)
|
||||
val response = apiClient.refreshToken()
|
||||
|
||||
when {
|
||||
response == null -> {
|
||||
LOG.error("Token refresh failed: null response")
|
||||
callback(false)
|
||||
}
|
||||
response.access_token != null -> {
|
||||
LOG.info("Token refresh successful")
|
||||
tokenManager.saveTokens(
|
||||
response.access_token,
|
||||
response.refresh_token!!,
|
||||
response.expires_in!!
|
||||
)
|
||||
callback(true)
|
||||
}
|
||||
else -> {
|
||||
LOG.error("Token refresh failed: ${response.message}")
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Token refresh error", e)
|
||||
callback(false)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform local username/password login
|
||||
*/
|
||||
@@ -116,7 +152,8 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
||||
LOG.info("Login successful")
|
||||
tokenManager.saveTokens(
|
||||
response.access_token,
|
||||
response.refresh_token!!
|
||||
response.refresh_token!!,
|
||||
response.expires_in!!
|
||||
)
|
||||
callback(true)
|
||||
}
|
||||
@@ -151,7 +188,8 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
||||
LOG.info("MFA verification successful")
|
||||
tokenManager.saveTokens(
|
||||
response.access_token,
|
||||
response.refresh_token!!
|
||||
response.refresh_token!!,
|
||||
response.expires_in!!
|
||||
)
|
||||
pendingMfaUsername = null
|
||||
callback(true)
|
||||
@@ -173,6 +211,13 @@ class EndurainSetupViewModel(application: Application) : AndroidViewModel(applic
|
||||
return tokenManager.getAccessToken() != null && !tokenManager.isTokenExpired()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token expiry date
|
||||
*/
|
||||
fun getTokenExpiresAt(): Date {
|
||||
return DateTimeUtils.parseTimeStamp(tokenManager.getAccessTokenExpiresAt())
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout and clear tokens
|
||||
*/
|
||||
|
||||
+4
-4
@@ -34,19 +34,19 @@ class EndurainTokenManager(context: Context) {
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
fun saveTokens(accessToken: String, refreshToken: String) {
|
||||
fun saveTokens(accessToken: String, refreshToken: String, accessTokenExpiresAt: Int) {
|
||||
sharedPreferences.edit {
|
||||
putString("access_token", accessToken)
|
||||
.putString("refresh_token", refreshToken)
|
||||
.putLong("expires_at", System.currentTimeMillis() + (15 * 60 * 1000))
|
||||
.putInt("expires_at", accessTokenExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAccessToken(): String? = sharedPreferences.getString("access_token", null)
|
||||
fun getRefreshToken(): String? = sharedPreferences.getString("refresh_token", null)
|
||||
fun getAccessTokenExpiresAt(): Int = sharedPreferences.getInt("expires_at", 0)
|
||||
fun isTokenExpired(): Boolean {
|
||||
val expiresAt = sharedPreferences.getLong("expires_at", 0)
|
||||
return System.currentTimeMillis() >= expiresAt
|
||||
return (System.currentTimeMillis() / 1000) >= getAccessTokenExpiresAt()
|
||||
}
|
||||
|
||||
fun clearTokens() {
|
||||
|
||||
Reference in New Issue
Block a user