Friday, August 16, 2013

Seamless Integration with the ArcGIS Javascript API Identity Manager (Token-Based)


Download: SecurityManager.zip
Introduction
The Esri Identity Manager control is ideal for scenarios where you need a login control for prompting your users for credentials. The control is fully responsible for generating a valid token from your Identity Provider and associating that token with your resources. This is perfect because all the complexities around security are handled by the Esri control allowing developers to be only concerned with the functionality they need to implement.
However, if your application needs to access services from different domains then you need to develop a proxy page or use CORS if supported by the browser. For further details refer to: https://developers.arcgis.com/en/javascript/jsapi/identitymanager-amd.html
There are also situations where you want to build your own Javascript application and seamlessly pass through your credentials at run-time and avoid the prompt dialog.
Because most people would develop a proxy page to solve these problems I’ve decided to attempt solving it with just JavaScript.
This can be useful in the following scenarios:
1 – You don’t have time to develop a proper and secure proxy page to integrate with your solution and with the ArcGIS stack.
2 – You have to provide access to your secure resources to applications that are out of your control and that are being implemented by developers that have no knowledge of ArcGIS and how to authenticate against it.
3 – You can’t provide the proxy page to these organizations because you cannot guarantee that it will be properly used/not compromised.
The javascript library in this post performs several functions including:
  • Extending the Esri IdentityManagerBase class with additional functionality.
  • Granting access to secure resources either coming from ArcGIS Online, Portal for ArcGIS or ArcGIS for Server (Federated or not).
  • Updating the token’s key before they expire by re-hydrating the state of the Identity Manager.
  • Providing a way of adding and removing secured resources from the list of items managed by the Security Manager control.
  • Supporting CORS using JSONP and jQuery.
  • Optimizing the performance of the application by only obtaining a token when required even if the browser is closed and opened again. Because the library is also executed client-side there is no need for having server-side functions for handling each request.
  • Supporting services coming from different servers as long as the credentials provided are valid to all servers.
Requirements:
  • Both the Web Server and the REST services NEED to be using HTTPS.
  • The credentials used are valid for all the resources registered with the Security Manager. If different servers are used make sure that the credentials are valid in all participating servers (e.g. ArcGIS for Server, Portal, etc).
  • If ArcGIS Online and Portal is used make sure that your credentials are valid for both these components and to the underlying ArcGIS for Server services.
  • When a resource is removed from the Security Manager it is destroyed and it is your responsibility to remove it from your application.
Below are provided two examples of how the Security Manager function can be used against resources coming from:
  • Portal/AGOL – Please change the credentials with your own. The credentials provided in the example cannot be used for testing for obvious reasons.
  • ArcGIS for Server – Fully workable example as the credentials provided can be used for testing
Portal or ArcGIS Online Example
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=7, IE=9, IE=10"/> 
        <title>Consume secured web map from Portal for ArcGIS</title>
        <link rel="stylesheet" href="https://serverapi.arcgisonline.com/jsapi/arcgis/3.5/js/dojo/dijit/themes/claro/claro.css">
        <link rel="stylesheet" href="https://serverapi.arcgisonline.com/jsapi/arcgis/3.5/js/esri/css/esri.css">
        <link rel="stylesheet" href="https://developers.arcgis.com/en/javascript/samples/ags_createwebmapid/css/layout.css">
        <script> var dojoConfig = { parseOnLoad: true }; </script>
        <script src="https://serverapi.arcgisonline.com/jsapi/arcgis/3.5/"></script>
        <script src="Scripts/jquery-2.0.0.min.js"></script>
        <script src="Scripts/security-1.0.2.min.js"></script>
        <script>
            dojo.require("dijit.layout.BorderContainer");
            dojo.require("dijit.layout.ContentPane");
            dojo.require("esri.map");
            dojo.require("esri.dijit.Legend");
            dojo.require("esri.dijit.Scalebar");

            var map;
            var resources = ["https://www.arcgis.com/sharing/rest/content/items/6d4047a85f674545a66666eaea44064f"];
            var webmapid = "6d4047a85f674545a66666eaea44064f";

            // PLEASE HIDE THE CREDENTIALS. THEY ARE SHOWN HERE FOR EASY UNDERSTANDING
            var credentials = { "userid": "jose", "username": "jose", "password": "pass1", "validity": 60 };  // validity is in minutes (>=1)

            function ready()
            {
                /// Manages the identity of the application by leveraging the Esri IdentityManager behind the scenes.
                /// @resources: Secured resources to be used. E.g.:
                /// Portal: ["https://www.arcgis.com/sharing/rest/content/items/0f6845e6e6bc4a84bee008a71c857df3"] 
                /// Server: ["https://mydomain/arcgis/rest/services/MyMap/MapServer","https://mydomain/arcgis/rest/services/MyMap2/MapServer"]
                /// @credentials: Credentials (e.g. { "userid": "jose", "username": "jose", "password": "jose", "validity": 60 } ). 
                ///               Note that validity cannot be inferior to 1. 
                /// @cache: Default is true. When set to true the class optimizes the performance of the application, reusing and negotiating 
                ///         new tokens only when required (e.g. when the token is expiring). 
                ///         If set to false, whenever the application restarts (or this method is called again) the class clears the cache 
                ///         before proceeding.
                /// @completed: The execution complete event handler.

                Security.ManageResources(resources, credentials, false, function ()  // change the cache to true when happy with the settings  
                {                                                                    // to avoid reusing older tokens
                    // YOUR CODE HERE
                    initializeApp();
                });
            };

            function initializeApp()
            {
                var request = esri.arcgis.utils.createMap(webmapid, "map");
                request.then(function (response)
                {
                    dojo.byId("title").innerHTML = response.itemInfo.item.title;
                    dojo.byId("subtitle").innerHTML = response.itemInfo.item.snippet;

                    map = response.map;

                    // get the layers that will display in the legend
                    var layers = esri.arcgis.utils.getLegendLayers(response);
                    console.log(layers);

                    if (map.loaded)
                    {
                        initMap(layers);
                    }
                    else
                    {
                        dojo.connect(map, "onLoad", function () { initMap(layers); });
                    }
                },
                function (error)
                {
                    console.log("Map creation failed: ", dojo.toJson(error));
                });
            }

            function initMap(layers)
            {
                //add a scalebar
                var scalebar = new esri.dijit.Scalebar({ map: map, scalebarUnit: "english" });

                //add a legend
                var legendDijit = new esri.dijit.Legend({   map: map, layerInfos: layers }, "legend");
                legendDijit.startup();
            }

            dojo.ready(ready);

        </script>
    </head>
    <body class="claro">
        <div id="mainWindow" data-dojo-type="dijit.layout.BorderContainer" data-dojo-props="design:'headline'" style="width:100%; height:100%;">
            <div id="header" class="shadow roundedCorners" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'top'">
                <div id="title"></div>
                <div id="subtitle"></div>
            </div>
            <div id="map" class="roundedCorners shadow" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'center'"></div>
            <div id="rightPane" class="roundedCorners shadow" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'right'" >
                <div id="legend"></div>
            </div>
        </div>
    </body>
</html>

ArcGIS for Server Example (Federated or not)

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=7, IE=9, IE=10"/> 
        <title>Consume secured services from ArcGIS for Server</title>
        <link rel="stylesheet" href="//serverapi.arcgisonline.com/jsapi/arcgis/3.5/js/dojo/dijit/themes/tundra/tundra.css">
        <link rel="stylesheet" href="//serverapi.arcgisonline.com/jsapi/arcgis/3.5/js/esri/css/esri.css">
        <style type="text/css">
            html, body 
            {
                height: 100%; 
                width: 100%;
                margin:0;
                padding:0;
            }
        </style>

        <script>var dojoConfig = { parseOnLoad: true };</script>
        <script src="Scripts/jquery-2.0.0.min.js"></script>        
        <script src="https://serverapi.arcgisonline.com/jsapi/arcgis/3.5/"></script>
        <script src="Scripts/security-1.0.2.min.js"></script>
        <script>
            dojo.require("dijit.layout.BorderContainer");
            dojo.require("dijit.layout.ContentPane");
            dojo.require("esri.map");
            dojo.require("esri.layers.FeatureLayer");

            var map;
            var resources = ["https://sampleserver6.arcgisonline.com/arcgis/rest/services/SaveTheBay/FeatureServer/0",
                             "https://sampleserver6.arcgisonline.com/arcgis/rest/services/SaveTheBay/FeatureServer/1"];

            // PLEASE HIDE THE CREDENTIALS. THEY ARE SHOWN HERE FOR EASY UNDERSTANDING
            var credentials = { "userid": "user1", "username": "user1", "password": "user1", "validity": 60 };  // >= 1 minutes.

            function ready()
            {
                /// Manages the identity of the application by leveraging the Esri IdentityManager behind the scenes.
                /// @resources: Secured resources to be used. E.g.:
                /// Portal: ["https://www.arcgis.com/sharing/rest/content/items/0f6845e6e6bc4a84bee008a71c857df3"] 
                /// Server: ["https://mydomain/arcgis/rest/services/MyMap/MapServer","https://mydomain/arcgis/rest/services/MyMap2/MapServer"]
                /// @credentials: Credentials (e.g. { "userid": "jose", "username": "jose", "password": "jose", "validity": 60 } ). 
                ///               Note that validity cannot be inferior to 1. 
                /// @cache: Default is true. When set to true the class optimizes the performance of the application, reusing and negotiating 
                ///         new tokens only when required (e.g. when the token is expiring). 
                ///         If set to false, whenever the application restarts (or this method is called again) the class clears the cache 
                ///         before proceeding.
                /// @completed: The execution complete event handler.

                Security.ManageResources(resources, credentials, false, function ()  // change the cache to true when happy with the settings
                {                                                                    // to avoid reusing older tokens
                    // YOUR CODE HERE
                    initializeApp();
                });
            };

            function initializeApp()
            {
                //map = new esri.Map("map");                
                map = new esri.Map("map", { basemap: "topo", center: [-107.394, 37.563], zoom: 9 });

                //var layer = new esri.layers.ArcGISDynamicMapServiceLayer(resources[0]);
                //map.addLayer(layer);

                var layer1 = new esri.layers.FeatureLayer(resources[0], { mode: esri.layers.FeatureLayer.MODE_ONDEMAND, outFields: ["*"] });
                map.addLayer(layer1);

                var layer2 = new esri.layers.FeatureLayer(resources[1], { mode: esri.layers.FeatureLayer.MODE_ONDEMAND, outFields: ["*"] });
                map.addLayer(layer2);

                // Shows how to use other features of the Security Utility
                //
                //if (map.loaded) OtherExamples();
                //else
                //    dojo.connect(map, "onLoad", function () { OtherExamples(); });

                //function OtherExamples()
                //{
                //    Security.RemoveResources(["https://sampleserver6.arcgisonline.com/arcgis/rest/services/SaveTheBay/FeatureServer/0"], function ()
                //    {
                //        Security.AddResources(["https://sampleserver6.arcgisonline.com/arcgis/rest/services/SaveTheBay/FeatureServer/0"], function ()
                //        {
                //            var layer2 = new esri.layers.FeatureLayer("https://sampleserver6.arcgisonline.com/arcgis/rest/services/SaveTheBay/FeatureServer/1", 
                //                                                      { mode: esri.layers.FeatureLayer.MODE_ONDEMAND, outFields: ["*"] });
                //            map.addLayer(layer2);
                //        });
                //    });
                //}
            };

            dojo.ready(ready);
      </script>
    </head>

    <body class="tundra">
        <div data-dojo-type="dijit.layout.BorderContainer" data-dojo-props="design:'headline', gutters:false" style="position:relative;width:100%;height:100%;">
            <div id="map" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'center'"></div>
        </div>
    </body>
</html>