DefaultAuthenticationChallengeHandler adding a AuthenticationChallengeResponse

944
10
10-07-2019 11:15 AM
AaronDick
Occasional Contributor

OK so I have DefaultAuthenticationChallengeHandler working fine to login to a runtime application with the following code which works great for logging in ...

AuthenticationChallengeHandler handler = new DefaultAuthenticationChallengeHandler(this);

OAuthConfiguration oAC = new OAuthConfiguration(urlLogin, Client_ID, "my-arcgis-app://auth", 129600); //90 days
AuthenticationManager.addOAuthConfiguration(oAC);
AuthenticationManager.setAuthenticationChallengeHandler(handler);

However the DefaultAuthenticationChallengeHandler works poorly for Web Maps in that the challenge occurs for every single layer in every feature service.  Nobody wants to login this many times.  When I use a custom login form for feature services I can suppress the multiple logins and use the first login to provide access to the rest of the layers in a feature service.  I can do this via a CountDownLatch to suppress the calling thread and wait for the thread firing off the login form for a specific feature service (see code below).  The issue I am having is I do like the DefaultAuthenticationChallengeHandler for login to the application because it allows me to not have to handle the username or password myself, but do not like the way it works with feature services. 

Is there a way to setup a AuthenticationChallengeResponse listener against the DefaultAuthenticationChallengeHandler so that I could add a CountDownLatch to the default login?

Below is using a custom login form to access a feature service with CountDownLatch.....

AuthenticationManager.setAuthenticationChallengeHandler(this);

@Override

public AuthenticationChallengeResponse handleChallenge(AuthenticationChallenge authenticationChallenge) {
try {
if (authenticationChallenge.getType() == AuthenticationChallenge.Type.USER_CREDENTIAL_CHALLENGE) {
if (((Portal) authenticationChallenge.getRemoteResource()).getLoadStatus() == LoadStatus.LOADED) {
return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CANCEL,
authenticationChallenge);
}

int maxAttempts = 5;
if (authenticationChallenge.getFailureCount() > maxAttempts) {
// exceeded maximum amount of attempts. Act like it was a cancel
Toast.makeText(this, "Exceeded maximum amount of attempts. Please try again!", Toast.LENGTH_LONG).show();
return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CANCEL,
authenticationChallenge);
}

String fsURL = authenticationChallenge.getRemoteResource().getUri();
if (!fsURL.endsWith("/FeatureServer")) {
if (fsURL.contains("/FeatureServer")) {
fsURL = fsURL.substring(0, fsURL.indexOf("/FeatureServer") + 14);
}
}

final String fsURLPassIN = fsURL;

if (mAgencyCredentials==null) {
authenticationCounter++;
DownloadHelper.signal = new CountDownLatch(1);

runOnUiThread(new Runnable() {
@Override
public void run() {
showFSLogin("Credential is required to access " + authenticationChallenge.getRemoteResource().getUri(), fsURLPassIN);
}
});

try {
DownloadHelper.signal.await();
} catch (InterruptedException e) {
String error = "Interruption handling AuthenticationChallengeResponse: " + e.getMessage();
Toast.makeText(this, error, Toast.LENGTH_LONG).show();
}
}
// if credentials were set, return a new auth challenge response with them. otherwise, act like it was a cancel
if (mAgencyCredentials != null) {
if (authenticationCounter==0) { //this should be only run once after authentication as this challenge response will hit once for each layer.
loadMap();
}
authenticationCounter++;
return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CONTINUE_WITH_CREDENTIAL, mAgencyCredentials);

}
}

} catch (Exception e){
e.printStackTrace();
}
// no credentials were set, return a new auth challenge response with a cancel
return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CANCEL, authenticationChallenge);

}

0 Kudos
10 Replies
XuemingWu
Esri Contributor

Hi Aaron,

Firing multiple challenges for layers hosted on the same server is known issue in our API and  has been planned to be fixed in  a future release. Sorry for the inconvenient.

To workaround this issue and also take advantage of the DefaultAuthenticationChallengeHandler, your custom challenge handler can extend the DefaultAuthenticationChallengeHandler. In handleChallenge() method of your custom challenge handler, you can return super.handleChallenge() where you want to let the DefaultAuthenticationChallengeHandler to take care of the challenges. For other challenges that you want to take care by yourself use your own logic like what you did in the code that you shared in this thread. 

Thanks.

0 Kudos
AaronDick
Occasional Contributor

Hi Xueming, Can you shed some light on how I would do this?  By extend do you mean implement?  Currently in my activity I can get  the

AuthenticationChallengeResponse handlechallenge(AuthenticationChallenge authenticationChallenge)  to work by adding 
this to my activity....

public class S1ViewerActivity extends FragmentActivity implements AuthenticationChallengeHandler

when I try to implement DefaultAuthenticationChallengeHandler it will not compile...

public class S1ViewerActivity extends FragmentActivity implements DefaultAuthenticationChallengeHandler



0 Kudos
XuemingWu
Esri Contributor

Hi Aaron,

The DefaultAuthenticationChallengeHandler is a class that implements AuthenticationChallengeHandler interface. You can extend it. The following is an example of extending the DefaultAuthenticationChallengeHandler:

  private class MyDefaultChallengeHandler extends DefaultAuthenticationChallengeHandler {
public MyDefaultChallengeHandler(Activity context) {
super(context);
}

@Override
public AuthenticationChallengeResponse handleChallenge(AuthenticationChallenge challenge) {
if (challenge.getRemoteResource() instanceof Portal) {
// let DefaultAuthenticationChallengeHandler handle this challenge
return super.handleChallenge(challenge);
} else {
// your logic to handle this challenge
......
}
}
}}

The add the following line to the onCreate() of your activity instead of implementing  AuthenticationChallengeHandler:

AuthenticationManager.setAuthenticationChallengeHandler(
new MyDefaultChallengeHandler(this));

Thanks.

AdamStewart
New Contributor II

I've followed this post since I'm having a similar problem; I am being authenticated against every FeatureLayer as OP is, but all authentications are working as expected on my Dev portal but only 1 out of 5 times my Production portal.  I've attempted to manage the 

else {
        // your logic to handle this challenge
        return new AuthenticationChallengeResponse( AuthenticationChallengeResponse.Action.CONTINUE_WITH_CREDENTIAL , portalCredential );

      }

by sending over the current credentials of the session where the portalCredential is set directly after the first authentication to Portal.  I can tell that there is a token with an expiration two hours in the future, but it always fails with "Token Required" as a result in the LOAD_FAILED check: 

portalMapLayers.get(0).getLoadError().getCause().getLocalizedMessage()

I'm convinced that the difference between my Dev Portal and my Production Portal is the time it returns a response.  Dev has no real load on it, and authenticates immediately.  Production takes an obvious fraction of a second, and when it's fast authenticates, but slow it fails and demands a new credential, over and over.

Is there any property that slows down any kind of layer.loadAsync() "timeout" that will let it try to load for a second or two before it assumes it is no longer authenticated?

0 Kudos
AaronDick
Occasional Contributor

Adam,  my full implementation code is below...I think CountDownLatch will help you as it allows you to wait for the thread running your login form to run before doing other things.  This avoids the login form from popping up multiple times.

I establish this variable in a helper class...

public static CountDownLatch signal;

Run this before you run your authentication login form on another thread...

DownloadHelper.signal = new CountDownLatch(1);

then right after this (not in the login form thread)

DownloadHelper.signal.await();

The code above forces it to wait for your login thread to to finish meaning you enter your credentials and click OK.

In the login class (my form) when I click the OK button and verify credentials I run this line....

DownloadHelper.signal.countDown();

After this the AuthenticationHandler class will hit for each feature service layer.  However at this point there is nothing you need to do and the layers will load correctly unless you have another feature service you are cycling through from a different token authentication service in your web map in which case you would get another prompt and the process would repeat itself.  Hope this helps a little.

import android.app.Activity;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;

import com.esri.arcgisruntime.data.Geodatabase;
import com.esri.arcgisruntime.portal.Portal;
import com.esri.arcgisruntime.security.AuthenticationChallenge;
import com.esri.arcgisruntime.security.AuthenticationChallengeResponse;
import com.esri.arcgisruntime.security.DefaultAuthenticationChallengeHandler;

import java.util.concurrent.CountDownLatch;

import gov.s1.s1mobile.downloadsync.DownloadHelper;
import gov.s1.s1mobile.downloadsync.MapExtentDownloadFS;
import gov.s1.s1mobile.downloadsync.MapExtentDownloadWM;
import gov.s1.s1mobile.managelayers.DeleteGDB;
import gov.s1.s1mobile.map.S1ViewerActivity;

import static gov.s1.s1mobile.map.S1ViewerActivity.mAgencyCredentials;

public class S1AuthenticationChallengeHandler extends DefaultAuthenticationChallengeHandler {
public S1AuthenticationChallengeHandler(Activity context) {
super(context);
}

private InterfaceCommunicator interfaceCommunicator; // To communicate with parent activity...

public interface InterfaceCommunicator {
void showFSLogin(String errorLabel, String fsURL);
}

public static Boolean backPressed = false; //used to prevent login screen from showing up when user backs out of web map preview window.
private int authenticationCounter = 0;

@Override
public AuthenticationChallengeResponse handleChallenge(AuthenticationChallenge challenge) {
if (challenge.getRemoteResource() instanceof Portal) {
// let DefaultAuthenticationChallengeHandler handle this challenge
return super.handleChallenge(challenge);
} else {
int maxAttempts = 5;
if (challenge.getFailureCount() > maxAttempts) {
// exceeded maximum amount of attempts. Act like it was a cancel
//Toast.makeText(this, "Exceeded maximum amount of attempts. Please try again!", Toast.LENGTH_LONG).show();
return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CANCEL,
challenge);
}

String fsURL = challenge.getRemoteResource().getUri();

if (!fsURL.endsWith("/FeatureServer")) {
if (fsURL.contains("/FeatureServer")) {
//singleLayerPublishedInteger = fsURL.substring(fsURL.lastIndexOf("/") + 1, fsURL.length());
fsURL = fsURL.substring(0, fsURL.indexOf("/FeatureServer") + 14);
}
}

final String fsURLPassIn = fsURL;
// create a countdown latch with a count of one to synchronize the dialog

if (mAgencyCredentials==null) { //&& backPressed==false) {
//authenticationCounter++;
DownloadHelper.signal = new CountDownLatch(1);

S1ViewerActivity.getS1ViewerActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {

if (!backPressed) {
if (FeatureServiceLogin.action!=null) {
if (FeatureServiceLogin.action.equals("downloadWM")) {
if (MapExtentDownloadWM.getMapExtentDownloadWM() != null) {
interfaceCommunicator = (InterfaceCommunicator) MapExtentDownloadWM.getMapExtentDownloadWM();
interfaceCommunicator.showFSLogin("Credential is required to access " + challenge.getRemoteResource().getUri(), fsURLPassIn);
}
} else if (FeatureServiceLogin.action.equals("download")) {
if (MapExtentDownloadFS.getMapExtentDownloadFS() != null) {
interfaceCommunicator = (InterfaceCommunicator) MapExtentDownloadFS.getMapExtentDownloadFS();
interfaceCommunicator.showFSLogin("Credential is required to access " + challenge.getRemoteResource().getUri(), fsURLPassIn);
}
} else if (FeatureServiceLogin.action.equals("delete")) {
DeleteGDB.getInstance().showFSLogin(fsURLPassIn);
//..showFSLogin("Credential is required to access " + challenge.getRemoteResource().getUri(), fsURLPassIn);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});

try {
DownloadHelper.signal.await();
} catch (InterruptedException e) {
String error = "Interruption handling AuthenticationChallengeResponse: " + e.getMessage();
//Toast.makeText(this, error, Toast.LENGTH_LONG).show();
}
}
// if credentials were set, return a new auth challenge response with them. otherwise, act like it was a cancel
if (mAgencyCredentials != null) {
return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CONTINUE_WITH_CREDENTIAL, mAgencyCredentials);
}


// your logic to handle this challenge
}
return null;
}
}
AaronDick
Occasional Contributor

I should probably explain this better.  When using this...

AuthenticationChallengeHandler handler = new DefaultAuthenticationChallengeHandler(this);

AuthenticationManager.setAuthenticationChallengeHandler(handler);

my custom handler response is never hit (even if I look at it prior to parsing if it is oauth or usercredential). 

If I use the following below then my custom handler does hit but then not using defaultauthenticationchallengehandler...

AuthenticationManager.setAuthenticationChallengeHandler(this);

So a little confused as to the steps I can take to get my custom handler response to respond to DefaultAuthenticationChallengeHandler?

0 Kudos
AaronDick
Occasional Contributor

Xueming, Thanks for the information.  That makes more sense.  Disregard my last response.

0 Kudos
AaronDick
Occasional Contributor

So a follow up question here.  I have the defaultauthenticationchallengehandler working but still would like to figure out how to close the Chrome tab.  I discovered a way to close the tab by using CustomTabsIntent.  However getting this to work with the challenge handler is a bit of a mystery.  Anyone have any thoughts on how to do this?   

import androidx.browser.customtabs.CustomTabsIntent;
 @Override
public AuthenticationChallengeResponse handleChallenge(AuthenticationChallenge challenge) {
if (challenge.getRemoteResource() instanceof Portal) {
// let DefaultAuthenticationChallengeHandler handle this challenge
//return super.handleChallenge(challenge); //How do we incorporate this challenge into CustomTabIntent?

String fsURL = challenge.getRemoteResource().getUri();
Uri uri = Uri.parse(fsURL);
Intent intent = new Intent(S1ViewerActivity.getAppContext(), CustomTabReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(S1ViewerActivity.getAppContext(), 0, intent, 0);

CustomTabsIntent.Builder customTabsBuilder = new CustomTabsIntent.Builder();
customTabsBuilder.addMenuItem("Close", pendingIntent);
CustomTabsIntent customTabsIntent = customTabsBuilder.build();
customTabsIntent.intent.setPackage("com.android.chrome");
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
customTabsIntent.launchUrl(S1ViewerActivity.getAppContext(), uri);

} else { //Not a portal attempt but secured feature service
//Do Stuff related to Feature Service
}
return null;
}
public class CustomTabReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent myIntent = new Intent(context, S1ViewerActivity.class);
myIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(myIntent);
}
}
0 Kudos
AaronDick
Occasional Contributor

Alright it is kind of looking like will have to work outside the context of the defaultauthenticationchallengehandler and handle all this myself from what I can tell.  Would have to add into the manifest something like the following...

<data android:scheme="com.packagename:/oauth"/> instead of using my-arcgis-app and then implement something like on this website...
https://medium.com/@ajinkyabadve/do-authentication-in-android-using-custom-chrome-tab-cct-2a0edffc93... 

The part that is pretty confusing is how to setup the redirect uri etc. Any thoughts greatly appreciated.

0 Kudos