diff --git a/plugins/arcgis/service/src/ArcGISIdentityManagerFactory.ts b/plugins/arcgis/service/src/ArcGISIdentityManagerFactory.ts new file mode 100644 index 000000000..e54b274bf --- /dev/null +++ b/plugins/arcgis/service/src/ArcGISIdentityManagerFactory.ts @@ -0,0 +1,99 @@ +import { ArcGISIdentityManager } from "@esri/arcgis-rest-request" +import { ArcGISAuthConfig, AuthType, FeatureServiceConfig, OAuthAuthConfig, TokenAuthConfig, UsernamePasswordAuthConfig } from './ArcGISConfig' +import { HttpClient } from "./HttpClient"; + +interface ArcGISIdentityManagerFactory { + create(portal: string, server: string, config: ArcGISAuthConfig, httpClient?: HttpClient): Promise +} + +const OAuthIdentityManagerFactory: ArcGISIdentityManagerFactory = { + async create(portal: string, server: string, auth: OAuthAuthConfig, httpClient: HttpClient): Promise { + console.debug('Client ID provided for authentication') + const { clientId, authToken, authTokenExpires, refreshToken, refreshTokenExpires } = auth + + if (authToken && new Date(authTokenExpires || 0) > new Date()) { + return ArcGISIdentityManager.fromToken({ + clientId: clientId, + token: authToken, + tokenExpires: new Date(authTokenExpires || 0), + portal: portal, + server: server + }) + } else if (refreshToken && new Date(refreshTokenExpires || 0) > new Date()) { + // TODO: find a way without using constructor nor httpClient + const url = `${portal}/oauth2/token?client_id=${clientId}&refresh_token=${refreshToken}&grant_type=refresh_token` + const response = await httpClient.sendGet(url) + // TODO: error handling + return ArcGISIdentityManager.fromToken({ + clientId: clientId, + token: response.access_token, + portal: portal + }); + // TODO: update authToken to new token + } else { + // TODO the config, we need to let the user know UI side they need to authenticate again + throw new Error('Refresh token missing or expired') + } + } +} + +const TokenIdentityManagerFactory: ArcGISIdentityManagerFactory = { + async create(portal: string, server: string, auth: TokenAuthConfig): Promise { + console.debug('Token provided for authentication') + const identityManager = await ArcGISIdentityManager.fromToken({ + token: auth.token, + portal: portal, + server: server, + // TODO: what do we really want to do here? esri package seems to need this optional parameter. + // Use authTokenExpires if defined, otherwise set to now plus a day + tokenExpires: auth.authTokenExpires ? new Date(auth.authTokenExpires) : new Date(Date.now() + 24 * 60 * 60 * 1000) + }) + return identityManager + } +} + +const UsernamePasswordIdentityManagerFactory: ArcGISIdentityManagerFactory = { + async create(portal: string, server: string, auth: UsernamePasswordAuthConfig): Promise { + console.debug('console and password provided for authentication, username:' + auth?.username) + const identityManager = await ArcGISIdentityManager.signIn({ username: auth?.username, password: auth?.password, portal }) + return identityManager + } +} + +const authConfigMap: { [key: string]: ArcGISIdentityManagerFactory } = { + [AuthType.OAuth]: OAuthIdentityManagerFactory, + [AuthType.Token]: TokenIdentityManagerFactory, + [AuthType.UsernamePassword]: UsernamePasswordIdentityManagerFactory +} + +export function getIdentityManager( + config: FeatureServiceConfig, + httpClient: HttpClient // TODO remove in favor of an open source lib like axios +): Promise { + const auth = config.auth + const authType = config.auth?.type + if (!auth || !authType) { + throw new Error('Auth type is undefined') + } + const factory = authConfigMap[authType] + if (!factory) { + throw new Error(`No factory found for type ${authType}`) + } + return factory.create(getPortalUrl(config.url), getServerUrl(config.url), auth, httpClient) +} + + +export function getPortalUrl(featureService: FeatureServiceConfig | string): string { + const url = getFeatureServiceUrl(featureService) + return `https://${url.hostname}/arcgis/sharing/rest` +} + +export function getServerUrl(featureService: FeatureServiceConfig | string): string { + const url = getFeatureServiceUrl(featureService) + return `https://${url.hostname}/arcgis` +} + +export function getFeatureServiceUrl(featureService: FeatureServiceConfig | string): URL { + const url = typeof featureService === 'string' ? featureService : featureService.url + return new URL(url) +} \ No newline at end of file diff --git a/plugins/arcgis/service/src/FeatureService.ts b/plugins/arcgis/service/src/FeatureService.ts index 46df9ddaa..51237c9fd 100644 --- a/plugins/arcgis/service/src/FeatureService.ts +++ b/plugins/arcgis/service/src/FeatureService.ts @@ -1,9 +1,6 @@ import { LayerInfoResult } from "./LayerInfoResult"; import { FeatureServiceResult } from "./FeatureServiceResult"; import { HttpClient } from "./HttpClient"; -import { AuthType, FeatureServiceConfig } from "./ArcGISConfig"; -import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"; -import { URL } from "node:url" /** * Queries arc feature services and layers. @@ -82,70 +79,3 @@ export class FeatureService { } } } - -export function getPortalUrl(featureService: FeatureServiceConfig | string): string { - const url = getFeatureServiceUrl(featureService) - return `https://${new URL(url).hostname}/arcgis/sharing/rest` -} - -export function getServerUrl(featureService: FeatureServiceConfig | string): string { - const url = getFeatureServiceUrl(featureService) - return `https://${url.hostname}/arcgis` -} - -export function getFeatureServiceUrl(featureService: FeatureServiceConfig | string): URL { - const url = typeof featureService === 'string' ? featureService : featureService.url - return new URL(url) -} - -export async function getIdentityManager( - featureService: FeatureServiceConfig, - httpClient: HttpClient // TODO remove in favor of an open source lib like axios -): Promise { - switch (featureService.auth?.type) { - case AuthType.Token: { - return ArcGISIdentityManager.fromToken({ - token: featureService.auth?.token, - portal: getPortalUrl(featureService), - server: getServerUrl(featureService) - }) - } - case AuthType.UsernamePassword: { - return ArcGISIdentityManager.signIn({ - username: featureService.auth?.username, - password: featureService.auth?.password, - portal: getPortalUrl(featureService), - }) - } - case AuthType.OAuth: { - // Check if feature service has refresh token and use that to generate token to use - const portal = getPortalUrl(featureService) - const { clientId, authToken, authTokenExpires, refreshToken, refreshTokenExpires } = featureService.auth - if (authToken && new Date(authTokenExpires || 0) > new Date()) { - return ArcGISIdentityManager.fromToken({ - clientId: clientId, - token: authToken, - tokenExpires: new Date(authTokenExpires || 0), - portal: getPortalUrl(featureService), - server: getServerUrl(featureService) - }) - } else { - if (refreshToken && new Date(refreshTokenExpires || 0) > new Date()) { - const url = `${portal}/oauth2/token?client_id=${clientId}&refresh_token=${refreshToken}&grant_type=refresh_token` - const response = await httpClient.sendGet(url) - // TODO: error handling - return ArcGISIdentityManager.fromToken({ - clientId: clientId, - token: response.access_token, - portal: portal - }); - // TODO: update authToken to new token - } else { - // TODO the config, we need to let the user know UI side they need to authenticate again - throw new Error('Refresh token missing or expired') - } - } - } - default: throw new Error('Authentication type not supported') - } -} \ No newline at end of file diff --git a/plugins/arcgis/service/src/index.ts b/plugins/arcgis/service/src/index.ts index 42d227f1a..495bbd1f0 100644 --- a/plugins/arcgis/service/src/index.ts +++ b/plugins/arcgis/service/src/index.ts @@ -12,7 +12,7 @@ import { ArcGISIdentityManager, request } from "@esri/arcgis-rest-request" import { FeatureServiceConfig } from './ArcGISConfig' import { URL } from "node:url" import express from 'express' -import { getIdentityManager, getPortalUrl } from './FeatureService' +import { getIdentityManager, getPortalUrl } from './ArcGISIdentityManagerFactory' const logPrefix = '[mage.arcgis]' const logMethods = ['log', 'debug', 'info', 'warn', 'error'] as const @@ -157,7 +157,7 @@ const arcgisPluginHooks: InitPluginHook = { console.info('Applying ArcGIS plugin config...') const arcConfig = req.body as ArcGISPluginConfig const configString = JSON.stringify(arcConfig) - console.info(configString) + console.info(configString) //TODO this puts user password in logs. Should we use debug or avoid logging? processor.putConfig(arcConfig) res.sendStatus(200) }) @@ -181,6 +181,7 @@ const arcgisPluginHooks: InitPluginHook = { try { const httpClient = new HttpClient(console) + // Create the IdentityManager instance to validate credentials await getIdentityManager(service!, httpClient) let existingService = config.featureServices.find(service => service.url === url) if (existingService) { @@ -192,7 +193,7 @@ const arcgisPluginHooks: InitPluginHook = { await processor.putConfig(config) return res.send(service) } catch (err) { - return res.send('Invalid username/password').status(400) + return res.send('Invalid credentials provided to communicate with feature service').status(400) } })