Skip to content

Commit

Permalink
Merge branch 'main' into 568-quest-list
Browse files Browse the repository at this point in the history
  • Loading branch information
pld authored Oct 8, 2021
2 parents 21e7039 + 2ae2f7e commit fe285c6
Show file tree
Hide file tree
Showing 14 changed files with 919 additions and 85 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added measure reporting to ANC application
- Added class for Measure report evaluation which will be used in ANC application
- ANC | Added Condition resource to sync params list
- Moved Token to secure storage from AccountManager
- QUEST | Patient List, Load Config from server
- QUEST | Added Patient Profile View

### Fixed

- Added dependecies that were missing and causing CQL Evaluation to fail
- Out of memory issue on few tests
- Authentication toekn expiry issue

### Changed

Expand Down
1 change: 1 addition & 0 deletions android/anc/src/main/res/xml/authenticator.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
android:accountType="@string/authenticator_account_type"
android:icon="@drawable/ic_launcher_foreground"
android:label="@string/app_name"
android:customTokens="true"
android:smallIcon="@drawable/ic_launcher_foreground" />
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ import android.os.Build
import android.os.Bundle
import androidx.core.os.bundleOf
import java.util.Locale
import org.smartregister.fhircore.engine.data.remote.model.response.OAuthResponse
import timber.log.Timber

class AccountAuthenticator(val context: Context, var authenticationService: AuthenticationService) :
class AccountAuthenticator(val context: Context, val authenticationService: AuthenticationService) :
AbstractAccountAuthenticator(context) {

var accountManager: AccountManager = AccountManager.get(context)
val accountManager: AccountManager = AccountManager.get(context)

override fun addAccount(
response: AccountAuthenticatorResponse,
Expand All @@ -58,43 +57,37 @@ class AccountAuthenticator(val context: Context, var authenticationService: Auth
response: AccountAuthenticatorResponse,
account: Account,
authTokenType: String,
options: Bundle
options: Bundle?
): Bundle {
var accessToken = accountManager.peekAuthToken(account, authTokenType)
var tokenResponse: OAuthResponse

Timber.i("Access token for user ${account.name} available:${accessToken?.isNotBlank()}")
var accessToken = authenticationService.getLocalSessionToken()

// Use saved refresh token to try to get new access token. Logout user otherwise
if (accessToken.isNullOrEmpty()) {
val savedRefreshToken = accountManager.getPassword(account)
Timber.i(
"Access token for user ${account.name}, account type ${account.type}, token type $authTokenType is available:${accessToken?.isNotBlank()}"
)

Timber.i("Saved refresh token is available: ${savedRefreshToken?.isNotBlank()}")
if (accessToken.isNullOrBlank()) {
// Use saved refresh token to try to get new access token. Logout user otherwise
authenticationService.getRefreshToken()?.let {
Timber.i("Saved active refresh token is available")

if (!savedRefreshToken.isNullOrEmpty()) {
runCatching {
authenticationService.refreshToken(savedRefreshToken)?.let { newTokenResponse ->
tokenResponse = newTokenResponse
accessToken = tokenResponse.accessToken
with(accountManager) {
setPassword(account, savedRefreshToken)
setAuthToken(account, authTokenType, accessToken)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notifyAccountAuthenticated(account)
}
}
authenticationService.refreshToken(it)?.let { newTokenResponse ->
accessToken = newTokenResponse.accessToken!!
authenticationService.updateSession(newTokenResponse)
}
}
.onFailure {
Timber.e("Refresh token expired before it was used", it.stackTraceToString())
return updateCredentials(response, account, authTokenType, options)
}
.onSuccess { Timber.i("Got new accessToken") }
}
}

if (!accessToken.isNullOrEmpty()) {
if (accessToken?.isNotBlank() == true) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
accountManager.notifyAccountAuthenticated(account)
}

return bundleOf(
Pair(AccountManager.KEY_ACCOUNT_NAME, account.name),
Pair(AccountManager.KEY_ACCOUNT_TYPE, account.type),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ package org.smartregister.fhircore.engine.auth
import kotlinx.serialization.Serializable

@Serializable
data class AuthCredentials(val username: String, val password: String, val sessionToken: String)
data class AuthCredentials(
val username: String,
val password: String,
var sessionToken: String,
var refreshToken: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ import org.smartregister.fhircore.engine.configuration.app.ApplicationConfigurat
import org.smartregister.fhircore.engine.data.remote.auth.OAuthService
import org.smartregister.fhircore.engine.data.remote.model.response.OAuthResponse
import org.smartregister.fhircore.engine.util.SecureSharedPreference
import org.smartregister.fhircore.engine.util.toSha1
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber

abstract class AuthenticationService(open val context: Context) {
val secureSharedPreference by lazy { SecureSharedPreference(context) }
val accountManager by lazy { AccountManager.get(context) }

fun getUserInfo(): Call<ResponseBody> =
OAuthService.create(context, getApplicationConfigurations()).userInfo()
Expand Down Expand Up @@ -80,13 +83,13 @@ abstract class AuthenticationService(open val context: Context) {
return payload
}

fun isSessionActive(token: String?): Boolean {
fun isTokenActive(token: String?): Boolean {
if (token.isNullOrEmpty()) return false
return try {
val tokenOnly = token.substring(0, token.lastIndexOf('.') + 1)
Jwts.parser().parseClaimsJwt(tokenOnly).body.expiration.after(Date())
} catch (expiredJwtException: ExpiredJwtException) {
Timber.w("Refresh/Access token expired", expiredJwtException)
Timber.w("Token is expired", expiredJwtException)
false
} catch (unsupportedJwtException: UnsupportedJwtException) {
Timber.w("JWT format not recognized", unsupportedJwtException)
Expand All @@ -97,46 +100,99 @@ abstract class AuthenticationService(open val context: Context) {
}
}

fun getLocalSessionToken(): String? {
Timber.v("Checking local storage for access token")
val token = secureSharedPreference.retrieveSessionToken()
return if (isTokenActive(token)) token else null
}

fun getRefreshToken(): String? {
Timber.v("Checking local storage for refresh token")
val token = secureSharedPreference.retrieveCredentials()?.refreshToken
return if (isTokenActive(token)) token else null
}

fun hasActiveSession(): Boolean {
Timber.v("Checking for an active session")
return getLocalSessionToken()?.isNotBlank() == true
}

fun validLocalCredentials(username: String, password: CharArray): Boolean {
Timber.v("Validating credentials with local storage")
return secureSharedPreference.retrieveCredentials()?.let {
it.username.contentEquals(username) &&
it.password.contentEquals(password.concatToString().toSha1())
}
?: false
}

fun updateSession(successResponse: OAuthResponse) {
Timber.v("Updating tokens on local storage")
val credentials =
secureSharedPreference.retrieveCredentials()!!.apply {
this.sessionToken = successResponse.accessToken!!
this.refreshToken = successResponse.refreshToken!!
}
secureSharedPreference.saveCredentials(credentials)
}

fun addAuthenticatedAccount(
accountManager: AccountManager,
successResponse: Response<OAuthResponse>,
username: String
username: String,
password: CharArray
) {
Timber.i("Adding authenticated account %s", username)
Timber.i("Adding authenticated account %s of type %s", username, getAccountType())

val accessToken = successResponse.body()!!.accessToken!!
val refreshToken = successResponse.body()!!.refreshToken!!

val account = Account(username, getAccountType())

accountManager.addAccountExplicitly(account, refreshToken, null)
accountManager.setAuthToken(account, AUTH_TOKEN_TYPE, accessToken)
accountManager.setPassword(account, refreshToken)
accountManager.addAccountExplicitly(account, null, null)
secureSharedPreference.saveCredentials(
AuthCredentials(username, password.concatToString().toSha1(), accessToken, refreshToken)
)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
accountManager.notifyAccountAuthenticated(account)
}
}

fun loadAccount(
accountManager: AccountManager,
username: String?,
fun getBlockingActiveAuthToken(): String? {
getLocalSessionToken()?.let {
return it
}
Timber.v("Trying to get blocking auth token from account manager")
return accountManager.blockingGetAuthToken(getActiveAccount(), AUTH_TOKEN_TYPE, false)
}

fun getActiveAccount(): Account? {
Timber.v("Checking for an active account stored")
return secureSharedPreference.retrieveSessionUsername()?.let { username ->
accountManager.getAccountsByType(getAccountType()).find { it.name.equals(username) }
}
}

fun loadActiveAccount(
callback: AccountManagerCallback<Bundle>,
errorHandler: Handler = Handler(Looper.getMainLooper(), DefaultErrorHandler)
) {
val accounts = accountManager.getAccountsByType(getAccountType())

if (accounts.isEmpty()) return
getActiveAccount()?.let { loadAccount(it, callback, errorHandler) }
}

val account = accounts.find { it.name.equals(username) } ?: return
Timber.i("Got account %s : ", account.name)
accountManager.getAuthToken(account, AUTH_TOKEN_TYPE, Bundle(), true, callback, errorHandler)
fun loadAccount(
account: Account,
callback: AccountManagerCallback<Bundle>,
errorHandler: Handler = Handler(Looper.getMainLooper(), DefaultErrorHandler)
) {
Timber.i("Trying to load from getAuthToken for account %s", account.name)
accountManager.getAuthToken(account, AUTH_TOKEN_TYPE, Bundle(), false, callback, errorHandler)
}

fun logout(accountManager: AccountManager) {
val account = accountManager.getAccountsByType(getAccountType()).firstOrNull()
fun logout() {
val account = getActiveAccount()

val refreshToken = getRefreshToken(accountManager)
val refreshToken = getRefreshToken()
if (refreshToken != null) {
OAuthService.create(context, getApplicationConfigurations())
.logout(clientId(), clientSecret(), refreshToken)
Expand All @@ -158,7 +214,7 @@ abstract class AuthenticationService(open val context: Context) {
}

fun cleanup() {
SecureSharedPreference(context).deleteCredentials()
secureSharedPreference.deleteCredentials()
context.startActivity(getLogoutUserIntent())
if (context is Activity) (context as Activity).finish()
}
Expand All @@ -173,11 +229,6 @@ abstract class AuthenticationService(open val context: Context) {
return intent
}

private fun getRefreshToken(accountManager: AccountManager): String? {
val account = accountManager.getAccountsByType(getAccountType()).firstOrNull()
return accountManager.getPassword(account)
}

abstract fun skipLogin(): Boolean

abstract fun getLoginActivityClass(): Class<*>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ package org.smartregister.fhircore.engine.data.remote.shared.interceptor

import android.content.Context
import okhttp3.Interceptor
import org.smartregister.fhircore.engine.util.SecureSharedPreference
import org.smartregister.fhircore.engine.configuration.app.ConfigurableApplication
import timber.log.Timber

class OAuthInterceptor(val context: Context) : Interceptor {
val authenticationService by lazy { (context as ConfigurableApplication).authenticationService }

override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
Timber.i("Intercepted request for auth headers if needed")
var request = chain.request()
val segments = mutableListOf("protocol", "openid-connect", "token")
if (!request.url.pathSegments.containsAll(segments)) {
val token = SecureSharedPreference(context).retrieveSessionToken()
// if (token.isNullOrEmpty()) throw IllegalStateException("No session token found")
Timber.i("Passing auth token for %s", request.url.toString())
request = request.newBuilder().addHeader("Authorization", "Bearer $token").build()
authenticationService.getBlockingActiveAuthToken()?.let { token ->
Timber.d("Passing auth token for %s", request.url.toString())
request = request.newBuilder().addHeader("Authorization", "Bearer $token").build()
}
}
return chain.proceed(request)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import org.smartregister.fhircore.engine.auth.AuthCredentials
import org.smartregister.fhircore.engine.auth.AuthenticationService
import org.smartregister.fhircore.engine.configuration.app.ConfigurableApplication
import org.smartregister.fhircore.engine.configuration.view.LoginViewConfiguration
import org.smartregister.fhircore.engine.data.remote.model.response.OAuthResponse
import org.smartregister.fhircore.engine.data.remote.shared.ResponseCallback
Expand All @@ -50,8 +48,6 @@ class LoginViewModel(
private val dispatcher: DispatcherProvider = DefaultDispatcherProvider
) : AndroidViewModel(application), AccountManagerCallback<Bundle> {

private val accountManager = AccountManager.get(application)

val responseBodyHandler =
object : ResponseHandler<ResponseBody> {
override fun handleResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
Expand All @@ -73,9 +69,6 @@ class LoginViewModel(
object : ResponseCallback<ResponseBody>(responseBodyHandler) {}
}

private val secureSharedPreference =
(application as ConfigurableApplication).secureSharedPreference

val oauthResponseHandler =
object : ResponseHandler<OAuthResponse> {
override fun handleResponse(call: Call<OAuthResponse>, response: Response<OAuthResponse>) {
Expand All @@ -88,10 +81,7 @@ class LoginViewModel(
return
}
with(authenticationService) {
addAuthenticatedAccount(accountManager, response, username.value!!)
secureSharedPreference.saveCredentials(
AuthCredentials(username.value!!, password.value!!, response.body()?.accessToken!!)
)
addAuthenticatedAccount(response, username.value!!, password.value?.toCharArray()!!)
getUserInfo().enqueue(userInfoResponseCallback)
_navigateToHome.value = true
_showProgressBar.postValue(false)
Expand All @@ -110,10 +100,10 @@ class LoginViewModel(
}

private fun attemptLocalLogin(): Boolean {
val (localUsername, localPassword) =
secureSharedPreference.retrieveCredentials() ?: return false
return (localUsername.contentEquals(username.value, ignoreCase = false) &&
localPassword.contentEquals(password.value))
return authenticationService.validLocalCredentials(
username.value!!,
password.value!!.toCharArray()
)
}

private val oauthResponseCallback: ResponseCallback<OAuthResponse> by lazy {
Expand Down Expand Up @@ -146,16 +136,11 @@ class LoginViewModel(

fun loginUser() {
viewModelScope.launch(dispatcher.io()) {
if (authenticationService.skipLogin() ||
authenticationService.isSessionActive(secureSharedPreference.retrieveSessionToken())
) {
if (authenticationService.skipLogin() || authenticationService.hasActiveSession()) {
Timber.v("Login not needed .. navigating to home directly")
_navigateToHome.postValue(true)
} else {
authenticationService.loadAccount(
accountManager,
secureSharedPreference.retrieveSessionUsername(),
this@LoginViewModel
)
authenticationService.loadActiveAccount(this@LoginViewModel)
}
}
}
Expand All @@ -177,7 +162,7 @@ class LoginViewModel(
override fun run(future: AccountManagerFuture<Bundle>?) {
val bundle = future?.result ?: bundleOf()
bundle.getString(AccountManager.KEY_AUTHTOKEN)?.run {
if (this.isNotEmpty() && authenticationService.isSessionActive(this)) {
if (this.isNotEmpty() && authenticationService.isTokenActive(this)) {
_navigateToHome.value = true
}
}
Expand Down
Loading

0 comments on commit fe285c6

Please sign in to comment.