We've built a web application with Experience Builder (frontend) and ASP.NET Core (backend). ASP.NET Core both serves the frontend at https://example.com/exb and provides custom REST API's at https://example.com/exb/api.
No content can be loaded unless the user has authenticated, including the JavaScript files that comprise the ExB app. When an unauthenticated user opens https://example.com/exb ASP.NET begins an a OAuth flow against ArcGIS Portal. Once signed in, ASP.NET returns a cookie that is required for all future requests to https://example.com/exb and child routes.
The access and refresh tokens one obtains from the OAuth flow with portal are embedded in the cookie. I can access these via a custom API (e.g., exb/api/token) to return these to ExB as needed.
We'd like to initialize ExB's SessionManager/ArcGISIdentityManager with our already signed in user as we need to prevent ExB from starting a secondary OAuth flow when accessing secured ArcGIS Enterprise services. We should be able to do this using the existing access & refresh token's but we're running into a bit of trouble. Here is our current attempt...
// login widget
import { React, SessionManager, type AllWidgetProps } from "jimu-core";
import type { IMConfig } from "../config";
import { useSignInFromToken } from "../hooks/auth";
const Widget = (props: AllWidgetProps<IMConfig>) => {
const clientId = "id-registered-in-arcgis-portal";
useSignInFromToken(props.portalUrl, clientId);
const mainSession = SessionManager.getInstance().getMainSession();
return (
<div style={{ backgroundColor: "whitesmoke" }}>
<p>Login</p>
<p>Portal: {mainSession?.portal ?? "unknown"} </p>
<p>User: {mainSession?.username ?? "unknown"}</p>
<p>Token: {mainSession?.token ?? "unknown"}</p>
</div>
);
};
export default Widget;
// auth.ts (hook)
import { ArcGISIdentityManager } from "@esri/arcgis-rest-request";
import { React, SessionManager } from "jimu-core";
import esriId from "@arcgis/core/identity/IdentityManager";
import { fetchToken } from "../api/token";
const { useEffect } = React;
export type AccessToken = string;
export function useSignInFromToken(portalUrl: string, clientId: string) {
useEffect(() => {
async function addOrReplaceSessionFromToken() {
try {
const accessTokenExpiration = new Date(Date.now());
accessTokenExpiration.setMinutes(accessTokenExpiration.getMinutes() + 30);
const userInfo = await fetchToken();
const idManager = await ArcGISIdentityManager.fromToken({
token: userInfo.accessToken,
clientId: clientId,
portal: `${portalUrl}/sharing/rest`,
username: userInfo.userName,
tokenExpires: accessTokenExpiration
// todo: fill these too?
// redirectUri: "",
// server: "",
});
const cred = idManager.toCredential();
esriId.registerToken(cred);
SessionManager.getInstance().addOrReplaceSession(idManager);
// was thinking this 👇 would create the JSAPITOAuth in Session Storage
// but it does not.
SessionManager.getInstance().initSession();
} catch (err) {
console.error("error", err);
}
}
addOrReplaceSessionFromToken();
}, [portalUrl, clientId]);
}
I've added the Widget to my experience as an always rendered widget. It's near the top of the Widget "stack", though I'm not sure that matters.
When this widget renders I fetch our access token, initialize a new ArcGISIdentityManager and add it to the SessionManager. However, sometimes ExB will still start it's own OAuth flow and other times it will not. This feels like a race condition. It seems that sometimes my widget doesn't finish setting the session before the Map and other widgets make their requests to Portal.
A Few Questions
Is there a way to force my code to run before all other widgets are rendered, either by setting up widget dependencies, or moving this code to another location?
Is the code above the correct way to initialize a session from an existing token? Do I need to include other parameters?
Why is the exb_auth local session variable created but not the JSAPIOAuth session variable? Do I need both for a fully hydrated session?
How can I properly handle access token refreshes? If the access token times out I need to get a new one. So long as the refresh token is valid I shouldn't have to have the user log in again. Can I repoint SessionManager/ArcGISIdentityManager to my own endpoint for getting a new access token?