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