Skip to content

Commit

Permalink
Add ArcGISIdentityManagerFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
Rick Saccoccia committed Oct 10, 2024
1 parent 5b51df4 commit ddffd1b
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 73 deletions.
99 changes: 99 additions & 0 deletions plugins/arcgis/service/src/ArcGISIdentityManagerFactory.ts
Original file line number Diff line number Diff line change
@@ -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<ArcGISIdentityManager>
}

const OAuthIdentityManagerFactory: ArcGISIdentityManagerFactory = {
async create(portal: string, server: string, auth: OAuthAuthConfig, httpClient: HttpClient): Promise<ArcGISIdentityManager> {
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<ArcGISIdentityManager> {
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<ArcGISIdentityManager> {
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<ArcGISIdentityManager> {
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)
}
70 changes: 0 additions & 70 deletions plugins/arcgis/service/src/FeatureService.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<ArcGISIdentityManager> {
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')
}
}
7 changes: 4 additions & 3 deletions plugins/arcgis/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -157,7 +157,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
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)
})
Expand All @@ -181,6 +181,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {

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) {
Expand All @@ -192,7 +193,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
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)
}
})

Expand Down

0 comments on commit ddffd1b

Please sign in to comment.