Select to view content in your preferred language

Accessing User Content From ArcGIS Rest API Using Custom Widget Getting Invalid Token Error

324
4
a month ago
Labels (1)
sakildev1
Emerging Contributor

Hello,

I was trying to explore user contents by following the documents https://developers.arcgis.com/rest/users-groups-and-items/user-content/, and I used the API https://machine.domain.com/webadaptor/sharing/rest/content/users/jsmith?sf=json&token=mytoken.

Option 1: When the token is generated from https://machine.domain.com/webadaptor/sharing/rest/generateToken, the content API is working perfectly from Postman, but from the widget, it is getting the error "Invalid token.".

sakildev1_0-1746903108352.png

Option 2: If the token is generated from Python, then the API is working perfectly, both from the widget and Postman

gis = GIS(portal_url, username, password)
print(gis._con.token)

I am seeking help to get a prompt solution to work with Option A because the token is generated from a .NET application.

Thank you
Sakil

0 Kudos
4 Replies
TimWestern
MVP

I have a few questions.

first: You said the 'token is generated from a .Net application'? Its my understanding that the rest endpoint for generateToken is an ArcGIS endpoint.  I don't know that it is specifically .Net but that should not matter.

second: You said a custom widget was having issues, could you show us the bit of code that makes the call to that endpoint?  Specifically, I'm wondering if you've not set it up to make the call asynchronously and wait for a callback.  


So I was expecting some kind of code which might look like this (note you'd have to have the correct url for the rest api endpoint, and its not wise to hard code username/pwd into a script like this for use in production, but you could use it to test out whether the endpoint is returning a token correctly)

async function generateToken(): Promise<string | null> {
  const tokenUrl = "https://machine.domain.com/webadaptor/sharing/rest/generateToken";

  const params = new URLSearchParams();
  params.append("username", "your-username");
  params.append("password", "your-password");
  params.append("client", "requestip"); // or 'referer' with referer param
  params.append("f", "json");
  params.append("expiration", "60"); // token lifetime in minutes

  try {
    const response = await fetch(tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body: params.toString()
    });

    const json = await response.json();
    if (json.token) {
      console.log("Token generated:", json.token);
      return json.token;
    } else {
      console.error("Token generation failed:", json);
      return null;
    }
  } catch (error) {
    console.error("Error generating token:", error);
    return null;
  }
}


Does this help?

0 Kudos
sakildev1
Emerging Contributor

Hi TimWestern,

Thank you for your response. I am sharing my code here:

From .NET:

Controller:

[HttpPost("generate-arcgis-token")]
public async Task<IActionResult> GetToken([FromBody] UserCreds payload)
{
try
{
string pass = CryptoHelper.DecryptStringFromCryptoJS(payload.pa_encp);
var tokenClient = new ArcGISAccessFacade();
var token = await tokenClient.GetArcGISTokenAsync(payload.portal_url, payload.username, pass);
return Ok(token);
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}


public async Task<TokenResponse> GetArcGISTokenAsync(string portal_url, string username, string password)
{
var url = portal_url + "/sharing/rest/generateToken";
int expirationMinutes = 60 * 24 * 7; // 7 days
var formData = new Dictionary<string, string>
{
{ "username", username },
{ "password", password },
{ "client", "referer" },
{ "referer", portal_url },
{ "expiration", expirationMinutes.ToString()},
{ "f", "json" }
};

var content = new FormUrlEncodedContent(formData);

var response = await httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();
var result = System.Text.Json.JsonSerializer.Deserialize<TokenResponse>(json);

return result;
}

From ExbWidget:

const GenerateToken = async (portalInfo: iPortalInfo): Promise<iPortalToken> => {
    const web_token_url = getConfigValue<string>('jps_web_api') + getConfigValue<string>('web_api_generate_gp_token')

    try {
        const response = await fetch(web_token_url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                portal_url: portalInfo.portal_url,
                username: portalInfo.username,
                pa_encp: portalInfo.pa_encp,
                server_service_url: portalInfo.server_service_url,
                gen_token_url: getConfigValue<string>('gen_token_gp_server_url')
            })
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error generating token:', error);
        throw error;
    }
};
 
const refreshToken = (portal_info) => {
    GenerateToken(portal_info)
      .then((token: iPortalToken) => {
        const tokenInfo = {
          token: token.token,
          expires: token.expires,
          ssl: token.ssl,
        } as iPortalToken
        console.log(tokenInfo);

        dispatch({
          type: 'setTokenInfo',
          val: tokenInfo
        });
      })
      .catch((error) => {
        console.error('Error generating token:', error);
      });
  };
 
This code returns a token. If I use the token in Postman is working fine, but it does not work with the user rest API from experience builder custom widget.

Finally, what I did that worked for me:
I have created a GP tool to get the token form here:

gis = GIS(portal_url, username, password)
print(gis._con.token)
 
Then I call this GP tool from .NET, and this token is now able to call users rest api.
0 Kudos
TimWestern
MVP

So I'm not sure I quite get the architecture here.

You have a custom widget in Experience Builder, (this is a separate web application built on top of ReactJS as a platform for multiple applications, which can then host multiple widgets custom and built-in)

You have some .Net application that you are trying to connect?

I'm wondering why the server would not see this other .Net Application as a different location.  The EXB app/Widget takes advantage of the user agent of the browser.  A .Net app's http call is likely going to look different. (I'm not sure what your .net application does once it has a token, but if all you need is to access the REST API to do some things in .Net I am struggling to understand why you need Experience Builder at all.)

Perhaps put Fiddler between the .Net application and the server and see the header differences might be a clue?  


0 Kudos
TimWestern
MVP

Looking at this bit of code:

public async Task<TokenResponse> GetArcGISTokenAsync(string portal_url, string username, string password)
{
var url = portal_url + "/sharing/rest/generateToken";
int expirationMinutes = 60 * 24 * 7; // 7 days
var formData = new Dictionary<string, string>
{
{ "username", username },
{ "password", password },
{ "client", "referer" },
{ "referer", portal_url },
{ "expiration", expirationMinutes.ToString()},
{ "f", "json" }
};

 

It occurss to me that you are using a 'referer' based token (using the portal_url)

Exb has some safeguards in there around that built around CORS, the Referer headers, as wella s portal trust level.  If you try to do the same with the same token from .NET you may find that won't work because the referer is going to look different.  (It might take a comparison between the request in the network tab that the widget makes vs the headers in the .net request).

The python one runs differently, so may not have some of the same checks I believe.

You might try using a different type instead of referrer

{ "client", "requestip" }


Especially if the ip seen by the .net app would be the same as that seen by EXB app?

The other option might be to register an OAuth App, setup a client_id and client_secret to generate a trusted token, and then use something server side (rather than in the code in the front end) to keep it secure when used.


0 Kudos