Integrated Windows authentication

View inJavaKotlinView on GitHubSample viewer app

Connect to an IWA secured Portal and search for maps.

Image of integrated windows authentication

Use case

Your organization might use Integrated Windows Authentication (IWA) to secure ArcGIS Enterprise. This can be useful because the same credentials used to log into your work computer and network can be used to authenticate with ArcGIS. IWA is built into Microsoft Internet Information Server (IIS) and works well for intranet applications but isn't always practical for internet apps.

How to use the sample

Enter the URL to your IWA-secured portal. Tap the button to search for web maps stored on the portal. You will be prompted for a user name, password, and domain. If you authenticate successfully, portal item results will display in the list. Select a web map item to display it in the map view.

How it works

  1. The AuthenticationManager object is configured with a challenge handler that will prompt for a Windows login (username, password, and domain) if a secure resource is encountered.
  2. When a search for portal items is performed against an IWA-secured portal, the challenge handler creates an UserCredential object from the information entered by the user.
  3. If the user authenticates, the search returns a list of web map PortalItems and the user can select one to display as an ArcGISMap.

Relevant API

  • UserCredential
  • Portal
  • AuthenticationManager

About the data

This sample searches for web map portal items on a secure portal. To successfully run the sample, you need:

  • Access to a portal secured with Integrated Windows Authentication that contains one or more web map items.
  • A login that grants you access to the portal.

Additional information

More information about IWA and its use with ArcGIS can be found at the following links:

Tags

authentication, security, Windows

Sample Code

MainActivity.javaMainActivity.javaPortalItemAdapter.java
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/* Copyright 2019 Esri
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.esri.arcgisruntime.sample.integratedwindowsauthentication;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.esri.arcgisruntime.ArcGISRuntimeEnvironment;
import com.esri.arcgisruntime.ArcGISRuntimeException;
import com.esri.arcgisruntime.concurrent.ListenableFuture;
import com.esri.arcgisruntime.loadable.LoadStatus;
import com.esri.arcgisruntime.mapping.ArcGISMap;
import com.esri.arcgisruntime.mapping.BasemapStyle;
import com.esri.arcgisruntime.mapping.view.MapView;
import com.esri.arcgisruntime.portal.Portal;
import com.esri.arcgisruntime.portal.PortalItem;
import com.esri.arcgisruntime.portal.PortalQueryParameters;
import com.esri.arcgisruntime.portal.PortalQueryResultSet;
import com.esri.arcgisruntime.security.AuthenticationChallenge;
import com.esri.arcgisruntime.security.AuthenticationChallengeHandler;
import com.esri.arcgisruntime.security.AuthenticationChallengeResponse;
import com.esri.arcgisruntime.security.AuthenticationManager;
import com.esri.arcgisruntime.security.UserCredential;

public class MainActivity extends AppCompatActivity implements AuthenticationChallengeHandler {

  private static final String TAG = MainActivity.class.getSimpleName();

  private RecyclerView mRecyclerView;

  private TextView mLoadWebMapTextView;

  private View mPortalLoadStateView;

  private TextView mLoadStateTextView;

  private MapView mMapView;

  private UserCredential mUserCredential;
  // objects that implement Loadable must be class fields to prevent being garbage collected before loading
  private Portal mPortal;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // authentication with an API key or named user is required to access basemaps and other
    // location services
    ArcGISRuntimeEnvironment.setApiKey(BuildConfig.API_KEY);

    // get a reference to the map view
    mMapView = findViewById(R.id.mapView);
    // create a streets base map
    ArcGISMap map = new ArcGISMap(BasemapStyle.ARCGIS_STREETS);
    // set the map to the map view
    mMapView.setMap(map);

    // set authentication challenge handler
    AuthenticationManager.setAuthenticationChallengeHandler(this);

    // set up recycler view for listing portal items
    mRecyclerView = findViewById(R.id.recyclerView);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

    // set up search public button
    Button searchPublicButton = findViewById(R.id.searchPublicButton);
    searchPublicButton.setOnClickListener(v -> {
      // search the the public ArcGIS portal
      mPortal = new Portal(getString(R.string.arcgis_url));
      searchPortal();
    });

    // get reference to load state UI elements
    mPortalLoadStateView = findViewById(R.id.portalLoadState);
    mPortalLoadStateView.setVisibility(View.GONE);
    mLoadStateTextView = findViewById(R.id.loadStateTextView);
    mLoadWebMapTextView = findViewById(R.id.loadedWebMapTextView);

    Button searchSecureButton = findViewById(R.id.searchSecureButton);
    EditText portalUrlEditText = findViewById(R.id.portalUrlEditText);
    searchSecureButton.setOnClickListener(v -> {
      // get the string entered for the secure portal URL.
      String securedPortalUrl = portalUrlEditText.getText().toString();
      if (!securedPortalUrl.isEmpty()) {
        // search an instance of the IWA-secured portal, the user may be challenged for access
        mPortal = new Portal(securedPortalUrl, true);
        searchPortal();
      } else {
        String error = "Portal URL is empty. Please enter a portal URL.";
        Toast.makeText(this, error, Toast.LENGTH_LONG).show();
        Log.e(TAG, error);
      }
    });
  }

  /**
   * Search the given portal for its portal items and display them in a recycler view. On click, call AddMap().
   *
   */
  private void searchPortal() {

    // check if the the portal is null
    if (mPortal == null) {
      Log.e(TAG, "Portal null");
      return;
    }

    // clear any existing data in the recycler view
    mRecyclerView.setAdapter(null);

    // show portal load state
    mPortalLoadStateView.setVisibility(View.VISIBLE);
    mLoadStateTextView.setText("Searching for web map items on the portal at " + mPortal.getUri());

    mPortal.loadAsync();
    mPortal.addDoneLoadingListener(() -> {
      if (mPortal.getLoadStatus() == LoadStatus.LOADED) {
        try {
          // update load state in UI with the portal URI
          mLoadStateTextView.setText("Connected to the portal on " + new URI(mPortal.getUri()).getHost());
        } catch (URISyntaxException e) {
          String error = "Error getting URI from portal: " + e.getMessage();
          Log.e(TAG, error);
        }
        // report the user name used for this connection.
        if (mPortal.getUser() != null) {
          mLoadStateTextView.setText("Connected as: " + mPortal.getUser().getUsername());
        } else {
          // for a secure portal, the user should never be anonymous
          mLoadStateTextView.setText("Connected as: Anonymous");
        }

        // search the portal for web maps
        ListenableFuture<PortalQueryResultSet<PortalItem>> portalItemResult = mPortal
            .findItemsAsync(new PortalQueryParameters("type:(\"web map\" NOT \"web mapping application\")"));
        portalItemResult.addDoneListener(() -> {
          try {
            PortalQueryResultSet<PortalItem> portalItemSet = portalItemResult.get();
            PortalItemAdapter portalItemAdapter = new PortalItemAdapter(portalItemSet.getResults(),
                portalItem -> addMap(mPortal, portalItem.getItemId()));
            mRecyclerView.setAdapter(portalItemAdapter);
            mPortalLoadStateView.setVisibility(View.GONE);
          } catch (ExecutionException | InterruptedException e) {
            // hide load state view
            mPortalLoadStateView.setVisibility(View.GONE);
            // report error
            String error = "Error getting portal item set from portal: " + e.getMessage();
            Toast.makeText(this, error, Toast.LENGTH_LONG).show();
            Log.e(TAG, error);
          }
        });
      } else {
        // hide load state view
        mPortalLoadStateView.setVisibility(View.GONE);
        // report error
        ArcGISRuntimeException loadError = mPortal.getLoadError();
        String error = "Portal sign in failed: " + loadError.getCause() == null ?
            loadError.getMessage() :
            loadError.getCause().getMessage();
        Toast.makeText(this, error, Toast.LENGTH_LONG).show();
        Log.e(TAG, error);
      }
    });
  }

  /**
   * Add the given portal item to a new map and set the map to the map view.
   *
   * @param portal
   * @param itemId
   */
  private void addMap(Portal portal, String itemId) {
    // report error and return if portal is null
    if (portal == null) {
      String error = "Portal not instantiated.";
      Toast.makeText(this, error, Toast.LENGTH_LONG).show();
      Log.e(TAG, error);
      return;
    }
    // use the item ID to create a portal item from the portal
    PortalItem portalItem = new PortalItem(portal, itemId);
    // create a map using the web map (portal item) and add it to the map view
    ArcGISMap webMap = new ArcGISMap(portalItem);
    mMapView.setMap(webMap);
    // show item ID in UI
    mLoadWebMapTextView.setText("Loaded web map from item " + itemId);
  }

  /**
   * When a user credential challenge is issued, a dialog will be presented to the user to take credential information.
   * The portal URL will be displayed as a message in the dialog. If a wrong credential has been passed in the previous
   * attempt, a different message will be displayed in the dialog. The dialog has two edit text boxes for username and
   * password respectively. Other SDKs' samples may have one more parameter for IWA domain. As indicated by the Javadoc
   * of UseCredential, the Android SDK is in favor of passing username as username@domain or domain\\username.
   */
  @Override
  public AuthenticationChallengeResponse handleChallenge(AuthenticationChallenge authenticationChallenge) {
    if (authenticationChallenge.getType() == AuthenticationChallenge.Type.USER_CREDENTIAL_CHALLENGE
        && authenticationChallenge.getRemoteResource() instanceof Portal) {

      // If challenge has been requested by a Portal and the Portal has been loaded, cancel the challenge
      // This is required as some layers have private portal items associated with them and we don't
      // want to auth against them
      if (((Portal) authenticationChallenge.getRemoteResource()).getLoadStatus() == LoadStatus.LOADED) {
        return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CANCEL,
            authenticationChallenge);
      }

      // inflate and create the credential dialog
      View dialogView = getLayoutInflater().inflate(R.layout.credential_dialog, null);
      AlertDialog.Builder builder = new AlertDialog.Builder(this);
      TextView hostname = dialogView.findViewById(R.id.credentialHostnameTextView);
      EditText username = dialogView.findViewById(R.id.credentialUsernameEditText);
      EditText password = dialogView.findViewById(R.id.credentialPasswordEditText);

      // create a countdown latch with a count of one to synchronize the dialog
      CountDownLatch signal = new CountDownLatch(1);
      runOnUiThread(() -> {
        // set click listeners
        builder.setPositiveButton("Sign In", (dialog, which) -> {
          if (username.getText().length() > 0 && password.getText().length() > 0) {
            // create user credential from edit text
            mUserCredential = new UserCredential(username.getText().toString(), password.getText().toString());
          } else {
            Toast.makeText(this, "Username and password must not be blank.", Toast.LENGTH_SHORT).show();
          }
          signal.countDown();
        }).setNegativeButton("Cancel", (dialog, which) -> {
          // user cancelled the sign in process. Reset credential to null
          mUserCredential = null;
          signal.countDown();
        }).setOnCancelListener(new DialogInterface.OnCancelListener() {
          @Override
          public void onCancel(DialogInterface dialog) {
            // act like it was a cancel. Reset credential to null
            mUserCredential = null;
            signal.countDown();
          }
        }).setView(dialogView);
        AlertDialog credentialDialog = builder.create();
        credentialDialog.setCanceledOnTouchOutside(false);
        credentialDialog.show();
        // set message text
        if (authenticationChallenge.getFailureCount() > 0) {
          hostname.setText("Wrong credential was passed to " + authenticationChallenge.getRemoteResource().getUri());
        } else {
          hostname.setText("Credential is required to access " + authenticationChallenge.getRemoteResource().getUri());
        }
      });
      try {
        signal.await();
      } catch (InterruptedException e) {
        String error = "Interruption handling AuthenticationChallengeResponse: " + e.getMessage();
        runOnUiThread(() -> {
          Toast.makeText(this, error, Toast.LENGTH_LONG).show();
        });
        Log.e(TAG, error);
      }

      // if credentials were set, return a new auth challenge response with them. otherwise, act like it was a cancel
      if (mUserCredential != null) {
        return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CONTINUE_WITH_CREDENTIAL,
            mUserCredential);
      }
    }
    // no credentials were set, return a new auth challenge response with a cancel
    return new AuthenticationChallengeResponse(AuthenticationChallengeResponse.Action.CANCEL, authenticationChallenge);
  }

  @Override
  protected void onPause() {
    mMapView.pause();
    super.onPause();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mMapView.resume();
  }

  @Override
  protected void onDestroy() {
    mMapView.dispose();
    super.onDestroy();
  }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.