OK so I have the following issue with the CredentialCache. When I access an on premise service the AuthenticationManager CredentialCache automatically adds in my credential, which includes username and password. When I close and reopen my Android application it retains the credential information in the CredentialCache and includes in plain text username and password...
AuthenticationManager.CredentialCache.toJson(); .
I know I can encrypt and store these credentials in a file for use between sessions but I would really like for the CredentialCache to not hang onto these credentials between sessions from a security standpoint.
I would like to have the CredentialCache clear when the application is closed. I know I can add code on the apps OnDestroy event to handle this as follows...
AuthenticationManager.CredentialCache.clear();
This is all well and good, but if the user force closes the application I am not able to clear the cache when the application terminates as I have no event for this (short of creating some sort of convoluted service to listen for a killing of the application).
Just wondering if there is something I am missing here? This seems like a security flaw that is hard to overcome with this implementation.
Hi Aaron,
It is indeed a flaw if the CredentialCache which stores credentials in-memory retains credentials information after an app is closed or forced closed. In our testing the CredentialCache was cleared out when an app was closed and reopened again. We may miss some workflow though. So, could you share your workflow or maybe some code snippets with us so that we can investigate.
We are ware of the security risks and concerns of storing credentials in plain texts. We introduced a new class called SharedPreferencesCredentialPersistence which encrypts and stores credentials in SharedPreferences file in 100.9.0. We also plan to provide a default implementation to persist credential cache to a secured storage which should be encrypted in the future.
Thanks,
Thanks for the information Xueming regarding SharedPreferencesCredentialPersistence. I am using the example provided here...
So trying this out I am experiencing a fatal crash every time that I try to set persistence(AuthenticationManager.CredentialCache.setPersistence(spcPersistence);) that appears to be related to the context. Here is my code...
try {
if(mValidLoginCredentials!=null) {
// setting persistance
SharedPreferencesCredentialPersistence spcPersistence = new SharedPreferencesCredentialPersistence(this);
CredentialCacheEntry ccEntry = new CredentialCacheEntry(AuthenticationManager.CredentialCache.toJson(), mValidLoginCredentials);
spcPersistence.add(ccEntry);
AuthenticationManager.CredentialCache.setPersistence(spcPersistence);
}
} catch(Exception e) {
e.printStackTrace();
}
Here is the fatal error I get on setPersistance. If I remove the line setPersistance there is no crash.
java.lang.IllegalArgumentException: Malformed server context
at com.esri.arcgisruntime.internal.e.a.a.m.c(SourceFile:215)
at com.esri.arcgisruntime.internal.e.a.a.m.a(SourceFile:97)
at com.esri.arcgisruntime.internal.m.i.a(SourceFile:133)
at com.esri.arcgisruntime.internal.m.i.b(SourceFile:556)
at com.esri.arcgisruntime.internal.m.i.lambda$Ot9dfO0N49gef9HDOrvF0DHIi4Y(Unknown Source:0)
at com.esri.arcgisruntime.internal.m.-$$Lambda$i$Ot9dfO0N49gef9HDOrvF0DHIi4Y.run(Unknown Source:2)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
I should have mentioned the Context (this) is based off of a FragmentActivity. The example on GitHub uses an AppCompatActivity. Not sure why this would matter, but perhaps this was never tested with a FragmentActivity?
OK by getting into the ESRI code this is where the issue seems to be as per the screen grab below...
Was just wondering if there is any resolution on this issue? Is this a bug?
Hi Aaron,
Sorry for missing your posts. Have been busy with upcoming release. I will try out the example and let you know if there is a solution and if it is a bug.
Regards!
Hi Aaron,
Thanks for reporting the issue. When the server context passed as a parameter to the constructor of CredentialCacheEntry does not have the proper pattern, our SDK will thrown an IllegalArgumentException. However this exception should not cause a crash when setting the persistence. It is a bug. The workaround is to pass in a server context with the right pattern. e.g. the server context of service "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Sync/SaveTheBaySync_secured/MapServer/0" is "sampleserver6.arcgisonline.com/arcgis".
One important thing I want to point out is that the example provided by us has not fully utilized the power of SharedPreferencesCredentialPersistence. The following are some Kotlin code snippets of a simpler example. The workflow of this example is:
1. Launch the activity
2. Init_persistence
3. Add_layer, you will be prompted for username/password to access the feature layer
4. Close & relaunch the activity
5. Init_persistence
6. Check_persistence, the feature layer should be displayed without any prompt for credentials
Hope this help.
Regards,
class MainActivity : AppCompatActivity() {
private val TAG = "PersistenceTest"
private val URL_SEPARATOR = "/"
private lateinit var credentialsStore : SharedPreferencesCredentialPersistence
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
AuthenticationManager.setAuthenticationChallengeHandler(
DefaultAuthenticationChallengeHandler(this)
)
val map = ArcGISMap(Basemap.createLightGrayCanvas())
mapView.map = map
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_activity, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.check_persistence -> {
displayLayer(getCredentialsFromPersistence("https://sampleserver6.arcgisonline.com/arcgis/rest/services/Sync/SaveTheBaySync_secured/MapServer"))
}
R.id.init_persistence ->
initPersistence()
R.id.add_layer ->
displayLayer(null)
R.id.cleanup -> {
AuthenticationManager.CredentialCache.clear()
mapView.map.operationalLayers.clear()
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun getCredentialsFromPersistence(url: String) : Credential? {
if (credentialsStore.credentials != null) {
Log.i(TAG, "Retrieving credentials from shared persistence ...")
for (entry in credentialsStore.credentials) {
val serverContext = getServerContext(url)
if (entry.serverContext.contentEquals(serverContext)) {
return entry.credential
}
}
}
return null
}
private fun initPersistence() {
val store = AuthenticationManager.CredentialCache.getPersistence()
Log.i(
TAG,
"initPersistence - Current credential cache - \n ${AuthenticationManager.CredentialCache.toJson()}"
)
if ( store == null) {
Log.i(TAG, "Current credential cache persistence is null. Setting a new one ...")
credentialsStore = SharedPreferencesCredentialPersistence(this)
AuthenticationManager.CredentialCache.setPersistence(credentialsStore)
} else {
credentialsStore = store as SharedPreferencesCredentialPersistence
}
}
private fun getServerContext(urlStr: String) : String {
val url = URL(urlStr)
var ret = url.path.toLowerCase()
try {
val pieces = ret.split(URL_SEPARATOR)
ret = url.host.toLowerCase()
return if (pieces.size < 3) {
pieces.joinToString(URL_SEPARATOR, ret)
} else {
if (pieces[1].isNotEmpty() && !pieces[1].startsWith(URL_SEPARATOR)) {
ret = ret.plus(URL_SEPARATOR)
}
ret.plus(pieces[1])
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return ret
}
private fun displayLayer(credential: Credential?) {
Log.i(
TAG,
"displayLayer - Current credential cache - \n ${AuthenticationManager.CredentialCache.toJson()}"
)
val mapImageLayer = ArcGISMapImageLayer(
"https://sampleserver6.arcgisonline.com/arcgis/rest/services/Sync/SaveTheBaySync_secured/MapServer"
)
if (credential != null) {
mapImageLayer.credential = credential
}
mapView.map.operationalLayers.add(mapImageLayer)
mapImageLayer.addDoneLoadingListener {
Log.i(TAG, "displayLayer - load status = ${mapImageLayer.loadStatus}")
}
}
override fun onResume() {
super.onResume()
mapView.resume()
}
override fun onPause() {
super.onPause()
mapView.pause()
}
override fun onDestroy() {
super.onDestroy()
mapView.dispose()
}
}