Select to view content in your preferred language

Implementing user authentication with Javascript API for Experience web app

73
5
Jump to solution
7 hours ago
MalinEngelhardt
New Contributor II

Hi,

I created an Experience with the ArcGIS Experience Builder Developer version, which is deployed on my server. Since I want users to use Analysis tools I have included in the Experience I need to implement user authentication. I want users to have a login button to then be able to view the map and use the tools in my experience.

I don't have much experience with authentication processes so I have not been able to include the authentication process properly into my Experience. So far I have come up with the following code:

 

<!doctype html>
<html lang="en-us">

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"/>
    <meta http-equiv="X-UA-Compatible" content="IE=EDGE"/>
    <meta name="google-site-verification" content="Gl928UcZRBt8t7njrpXivrsVrbVcc7sLybtCMZ6Dzlg"/>
    <title>Web GIS</title>
    <link rel="shortcut icon" href="assets/exb.ico">
    <link rel="stylesheet" href="https://js.arcgis.com/4.29/esri/themes/light/main.css">
	<base href="./cdn/4/"/>
	<script src="https://cdn.jsdelivr.net/npm/bowser@2.11.0/es5.js"></script>
    <style>
        * {
            scrollbar-color: var(--light-600) transparent;
        }
        html, body {
            width: 100%;
            height: 100%;
            margin: 0;
            overflow: hidden;
        }
        html.scrollable {
            overflow: auto;
        }
        html.scrollable body {
            overflow: unset;
        }
        html.scrollable body, html.scrollable #app {
            height: auto;
        }
        #loading {
            width: 100vw;
            height: 100vh;
        }
        #app {
            width: 100%;
            height: 100%;
            position: relative;
            z-index: 0;
            display: none;
        }
        .auth-buttons {
            position: absolute;
            top: 10px;
            right: 10px;
            z-index: 10;
        }
        .auth-buttons button {
            margin-left: 10px;
        }
    </style>
</head>

<body>
    <div id="loading">
        <style>
            .loading-content {
                position: absolute;
                top: 0;
                bottom: 0;
                left: 0;
                right: 0;
            }
            .jimu-primary-loading-app:before, .jimu-primary-loading-app:after {
                position: absolute;
                top: 0;
                content: '';
            }
            .jimu-primary-loading-app:before {
                left: -19.992px;
            }
            .jimu-primary-loading-app:after {
                left: 19.992px;
                -webkit-animation-delay: 0.32s !important;
                animation-delay: 0.32s !important;
            }
            .jimu-primary-loading-app:before, .jimu-primary-loading-app:after, .jimu-primary-loading-app {
                background: #076fe5;
                -webkit-animation: loading-keys-app-loading 0.8s infinite ease-in-out;
                animation: loading-keys-app-loading 0.8s infinite ease-in-out;
                width: 13.6px;
                height: 32px;
            }
            .jimu-primary-loading-app {
                text-indent: -9999em;
                margin: auto;
                position: absolute;
                right: calc(50% - 6.8px);
                top: calc(50% - 16px);
                -webkit-animation-delay: 0.16s !important;
                animation-delay: 0.16s !important;
            }
            @-webkit-keyframes loading-keys-app-loading {
                0%, 80%, 100% { opacity: .75; box-shadow: 0 0 #076fe5; height: 32px; }
                40% { opacity: 1; box-shadow: 0 -8px #076fe5; height: 40px; }
            }
            @keyframes loading-keys-app-loading {
                0%, 80%, 100% { opacity: .75; box-shadow: 0 0 #076fe5; height: 32px; }
                40% { opacity: 1; box-shadow: 0 -8px #076fe5; height: 40px; }
            }
        </style>
        <div class="loading-content">
            <div class="justify-content-center jimu-primary-loading-app"></div>
        </div>
    </div>

    <div id="app"></div>

    <div class="auth-buttons">
        <button id="sign-in" class="btn btn-primary">Sign In</button>
        <button id="sign-out" class="btn btn-primary" style="display:none;">Sign Out</button>
    </div>

    <pre><code id="results"></code></pre>

    <script type="webpack-options" id="webpack-options">{
        "mountPath": "/",
        "isOutOfExb": true,
        "hostEnv": "prod",
        "appFolderName": ".",
        "useStructuralUrl": false,
        "arcgisJsApiUrl": "https://js.arcgis.com/4.29/",
        "isDevEdition": true,
        "buildNumber": "1",
        "isBuilder": false,
        "isSite": false,
        "isInPortal": false
    }</script>

    <script type="systemjs-importmap">{
        "imports": {
            "jimu-core": "https://BASE_URL/jimu-core/index.js",
            "jimu-core/": "https://BASE_URL/jimu-core/",
            "jimu-theme": "https://BASE_URL/jimu-theme/index.js",
            "jimu-theme/": "https://BASE_URL/jimu-theme/",
            "jimu-ui": "https://BASE_URL/jimu-ui/index.js",
            "jimu-ui/": "https://BASE_URL/jimu-ui/",
            "jimu-icons/": "https://BASE_URL/jimu-icons/",
            "jimu-arcgis": "https://BASE_URL/jimu-arcgis/index.js",
            "jimu-arcgis/": "https://BASE_URL/jimu-arcgis/",
            "jimu-layouts": "https://BASE_URL/jimu-layouts/index.js",
            "jimu-layouts/": "https://BASE_URL/jimu-layouts/",
            "jimu-for-builder": "https://BASE_URL/jimu-for-builder/index.js",
            "jimu-for-builder/": "https://BASE_URL/jimu-for-builder/",
            "widgets/": "https://BASE_URL/widgets/",
            "themes/": "https://BASE_URL/themes/",
            "builder/": "https://BASE_URL/builder/",
            "site/": "https://BASE_URL/site/",
            "experience/": "https://BASE_URL/experience/",
            "template/": "https://BASE_URL/template/",
            "templates/": "https://BASE_URL/templates/",
            "calcite-components": "https://BASE_URL/calcite-components/index.js",
            "calcite-components/": "https://BASE_URL/calcite-components/",
            "arcgis-charts": "https://BASE_URL/arcgis-charts/arcgis-charts.js",
            "arcgis-amd-packages/": "https://amd-packages/",
            "esri/": "https://API_URL/esri/"
        }
    }</script>

    <script src="./jimu-core/init.js"></script>

    <!-- Authentication script -->
    <script>
        require([
            "esri/portal/Portal",
            "esri/identity/OAuthInfo",
            "esri/identity/IdentityManager"
        ], function (Portal, OAuthInfo, esriId) {

            const info = new OAuthInfo({
                appId: "My-APP-ID", // removed due to privacy reasons
                popup: false // Use false if you want to redirect instead of using a popup
            });
            esriId.registerOAuthInfos([info]);

            esriId.checkSignInStatus(info.portalUrl + "/sharing")
                .then(() => {
                    handleSignedIn();
                })
                .catch(() => {
                    handleSignedOut();
                });

            document.getElementById("sign-in").addEventListener("click", function () {
                esriId.getCredential(info.portalUrl + "/sharing");
            });

            document.getElementById("sign-out").addEventListener("click", function () {
                esriId.destroyCredentials();
                window.location.reload();
            });

            function handleSignedIn() {
                const portal = new Portal();
                portal.load().then(() => {
                    const results = {
                        name: portal.user.fullName,
                        username: portal.user.username
                    };
                    document.getElementById("results").innerText = JSON.stringify(results, null, 2);
                    document.getElementById("sign-in").style.display = "none";
                    document.getElementById("sign-out").style.display = "block";
                });
            }

            function handleSignedOut() {
                document.getElementById("results").innerText = 'Signed Out';
                document.getElementById("sign-in").style.display = "block";
                document.getElementById("sign-out").style.display = "none";
            }
        });
    </script>

</body>

</html>

 

This will load my Experience and display a button, but I can't log in due to a ReferenceError: require is not defined. Adding <script src="https://js.arcgis.com/4.29/"></script> in the header will result in a MultipleDefines Error instead.

Any help would be appreciated.

0 Kudos
1 Solution

Accepted Solutions
MalinEngelhardt
New Contributor II

Thank you for your answer. Unfortunately, I was unable to get either method to work. However, after adding <script src="https://js.arcgis.com/4.29/"></script> into the body of the file, my issues seem to be resolved for now.

View solution in original post

5 Replies
CodyPatterson
Regular Contributor

Hey @MalinEngelhardt 

The MutipleDefines appears that it comes from the link that you're adding into the header, as require is defined many times.

Instead of require, you could use the System.import method to attempt and load the information in:

System.import('esri/portal/Portal').then(function (Portal) {
    System.import('esri/identity/OAuthInfo').then(function (OAuthInfo) {
        System.import('esri/identity/IdentityManager').then(function (esriId) {

If this doesn't end up working, you could attempt to import the functions at the top of your script, as in standard JS.

import <Portal> from <esri/portal/Portal>

And so on.

Hope that helps!

Cody

0 Kudos
MalinEngelhardt
New Contributor II

Thank you for your answer. Unfortunately, I was unable to get either method to work. However, after adding <script src="https://js.arcgis.com/4.29/"></script> into the body of the file, my issues seem to be resolved for now.

KenBuja
MVP Esteemed Contributor

In my custom widget that I am current working on (so I don't have everything in place, such as the settings panel), I have a button to authenticate and return a specific layer that is shared with the user.

It's a separate component from the main widget, which is imported and used like this:

...
import SignIn from '../components/SignIn';
...

return (
  ...
    <SignIn getUrl={getUrl} {...props}></SignIn> //getUrl is a function that uses the url in the component to make a feature layer and add it to the map.

This is the SignIn component. Since I haven't worked on the settings panel, I use tempConfig in its place.

import { AllWidgetProps } from 'jimu-core';
import { Alert, Button } from 'jimu-ui';
import React, { useState } from 'react';
import OAuthInfo from '@arcgis/core/identity/OAuthInfo';
import esriId from '@arcgis/core/identity/IdentityManager';
import esriConfig from '@arcgis/core/config';
import Portal from '@arcgis/core/portal/Portal';
import PortalQueryParams from '@arcgis/core/portal/PortalQueryParams';
import tempconfig from '../assets/test_config.json';
import { IMConfig } from '../config';
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';

export default function SignIn({ getUrl }, props: AllWidgetProps<IMConfig>) {
  const [buttonText, setButtonText] = useState('Sign In');
  const [signedIn, setSignedIn] = useState(false);
  const [welcomeText, setWelcomeText] = useState('');
  const [showAlert, setShowAlert] = useState(false);
  const [alertMessage, setAlertMessage] = useState('');

  let gridUrl: string = null;
  let _portal: Portal;
  let _ownerName: string = tempconfig.agolSettings.ownerName;
  let _baseGridLayerName: string = tempconfig.agolSettings.baseGrid;
  const portalUrl = tempconfig.agolSettings.portalUrl; //your portal
  const appInfo = tempconfig.agolSettings.auths;  //see below
  let theAppId: string, thePopupCallbackUrl: string;

  //functions

  const formSubmit = () => {
    if (signedIn) {
      logOff();
    } else {
      logOn();
    }
  };

  const logOn = () => {
    setSignedIn(true);
    setButtonText('Sign Out');
    setShowAlert(false);
    const loc = window.location.hostname;
    const location = appInfo.find(
      (x) => x.location === window.location.hostname
    );
    if (location !== undefined) {
      theAppId = location.appId;
      thePopupCallbackUrl = location.popupCallbackUrl;
    } else {
      setAlertMessage(
        `This site (${window.location.hostname}) is not in the Authentication Location table in the Settings.`
      );
      setShowAlert(true);
      logOff();
      return;
    }

    const info = new OAuthInfo({
      appId: theAppId,
      flowType: 'auto',
      popup: false,
    });
    esriConfig.request.trustedServers.push(portalUrl);
    esriId.registerOAuthInfos([info]);
    try {
      esriId
        .getCredential(info.portalUrl, { oAuthPopupConfirmation: false })
        .then(() => {
          _portal = new Portal({ url: portalUrl });

          _portal.load().then(() => {
            const queryParams: PortalQueryParams = {
              num: 100,
              query: `owner: ${_ownerName} AND type: "Feature Service" AND title: "${_baseGridLayerName}"`, //this finds the specific layer in AGOL
              sortField: 'title',
              sortOrder: 'asc',
            };
            _portal.queryItems(queryParams).then((results) => {
              if (results.total == 0) {
                setAlertMessage('The layer is not available');
                setShowAlert(true);
                logOff();
                return;
              }
              if (
                !results.results.some((item) => {
                  if (item.url == null) return false;
                  gridUrl = item.url;
                  return item.url.indexOf(_baseGridLayerName) > -1;
                })
              ) {
                setAlertMessage(
                  'You are not authorized to use this layer!'
                );
                setShowAlert(true);
                logOff();
                return;
              }
              setWelcomeText(`Welcome ${_portal.user.fullName}`);
              getUrl(gridUrl);
            });
          });
        });
    } catch ({ name, message }) {
      console.log(`***********${name}`);
      console.log(message);
    }
  };

  const logOff = () => {
    setSignedIn(false);
    setWelcomeText('');
    setButtonText('Sign In');
    esriId.destroyCredentials();
    getUrl('');;
  };

  const styles = {
    wrapper: {
      display: 'flex',
      margin: '10px',
      alignItems: 'center',
      justifyContent: 'space-between',
    },
    welcome: {
      marginLeft: 'auto',
    },
    alert: {
      margin: '10px',
      width: 'auto',
    },
  };
  return (
    <>
      <div className="wrapper" style={styles.wrapper}>
        {/* <form onSubmit={formSubmit}> */}
        <Button onClick={formSubmit}>{buttonText}</Button>
        <div className="text" style={styles.welcome}>
          {welcomeText}
        </div>
      </div>
      <Alert
        style={styles.alert}
        text={alertMessage}
        type="error"
        open={showAlert}
      />
    </>
  );
}

 On Line 25, appInfo is an array of possible locations where the user is signing in from, along with the appId set up for that location. That looks like this:

  "auths": [
    {
      "location": "localhost",
      "appId": "xxxxxxxxxxxxxxxx"
    },
    {
      "location": "development server url",
      "appId": "yyyyyyyyyyyyyyyy"
    },
    {
      "location": "production server url",
      "appId": "zzzzzzzzzzzzzzzz"
    }
0 Kudos
KenBuja
MVP Esteemed Contributor

Since I can't current edit my own post, please note that "auths" looks like this

  "auths": [
    {
      "location": "localhost",
      "appId": "xxxxxxxxxxxxxxxx",
      "popupCallbackUrl": "../assets/oauth-callback.html"
    },
    {
      "location": "development server url",
      "appId": "yyyyyyyyyyyyyyyy",
      "popupCallbackUrl": "../assets/oauth-callback.html"
    },
    {
      "location": "production server url",
      "appId": "zzzzzzzzzzzzzzzz",
      "popupCallbackUrl": "../assets/oauth-callback.html"
    }
0 Kudos
MalinEngelhardt
New Contributor II

Thank you for your answer. While I have resolved my issue, moving the login into a custom widget will be something I will look into.

0 Kudos