Skip to content

Commit

Permalink
Merge branch 'develop' into switch-httpClient-to-esri-library
Browse files Browse the repository at this point in the history
  • Loading branch information
newmanw committed Nov 1, 2024
2 parents feeea9e + af4a7c9 commit c38a342
Show file tree
Hide file tree
Showing 24 changed files with 605 additions and 459 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ MAGE adheres to [Semantic Versioning](http://semver.org/).
- Protect against disabling all authentications.
- Problem with OAuth web login

## [6.2.13](https://github.com/ngageoint/mage-server/releases/tag/6.2.13)
### Service
#### Features
* Support absolute callback URL configuration for SAML authentication module.
* Remove long deprecated `/authorize` device authorization endpoint from SAML authentication module.
### Web App
#### Features
* Add _Redirect Host_ field to SAML configuration form to align with Service SAML change.

## [6.2.12](https://github.com/ngageoint/mage-server/releases/tag/6.2.12)
### Service
#### Security
Expand Down
5 changes: 4 additions & 1 deletion plugins/arcgis/service/src/ArcGISConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,14 @@ export interface UsernamePasswordAuthConfig {
* Contains OAuth authentication configuration.
*/
export interface OAuthAuthConfig {

type: AuthType.OAuth

/**
* The Client Id for OAuth
*/
clientId: string

/**
* The redirectUri for OAuth
*/
Expand All @@ -121,7 +124,7 @@ export interface OAuthAuthConfig {
*/
authToken?: string

/**
/**
* The expiration date for the temporary token
*/
authTokenExpires?: number
Expand Down
49 changes: 34 additions & 15 deletions plugins/arcgis/service/src/ArcGISIdentityManagerFactory.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"
import { ArcGISIdentityManager, request } from "@esri/arcgis-rest-request"
import { ArcGISAuthConfig, AuthType, FeatureServiceConfig, OAuthAuthConfig, TokenAuthConfig, UsernamePasswordAuthConfig } from './ArcGISConfig'
import { HttpClient } from "./HttpClient";
import { ObservationProcessor } from "./ObservationProcessor";

interface ArcGISIdentityManagerFactory {
create(portal: string, server: string, config: ArcGISAuthConfig, httpClient?: HttpClient): Promise<ArcGISIdentityManager>
create(portal: string, server: string, config: ArcGISAuthConfig, processor?: ObservationProcessor): Promise<ArcGISIdentityManager>
}

const OAuthIdentityManagerFactory: ArcGISIdentityManagerFactory = {
async create(portal: string, server: string, auth: OAuthAuthConfig, httpClient: HttpClient): Promise<ArcGISIdentityManager> {
async create(portal: string, server: string, auth: OAuthAuthConfig, processor: ObservationProcessor): Promise<ArcGISIdentityManager> {
console.debug('Client ID provided for authentication')
const { clientId, authToken, authTokenExpires, refreshToken, refreshTokenExpires } = auth

Expand All @@ -22,14 +22,33 @@ const OAuthIdentityManagerFactory: ArcGISIdentityManagerFactory = {
} 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
try {
const response = await request(url, {
httpMethod: 'GET'
});

// Update authToken to new token
const config = await processor.safeGetConfig();
let service = config.featureServices.find(service => service.url === portal)?.auth as OAuthAuthConfig;
const date = new Date();
date.setSeconds(date.getSeconds() + response.expires_in || 0);
service = {
...service,
authToken: response.access_token,
authTokenExpires: date.getTime()
}

await processor.putConfig(config)
return ArcGISIdentityManager.fromToken({
clientId: clientId,
token: response.access_token,
tokenExpires: date,
portal: portal
});
} catch (error) {
throw new Error('Error occurred when using refresh 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')
Expand All @@ -46,7 +65,7 @@ const TokenIdentityManagerFactory: ArcGISIdentityManagerFactory = {
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)
tokenExpires: auth.authTokenExpires ? new Date(auth.authTokenExpires) : new Date(Date.now() + 24 * 60 * 60 * 1000)
})
return identityManager
}
Expand All @@ -68,7 +87,7 @@ const authConfigMap: { [key: string]: ArcGISIdentityManagerFactory } = {

export function getIdentityManager(
config: FeatureServiceConfig,
httpClient: HttpClient // TODO remove in favor of an open source lib like axios
processor: ObservationProcessor
): Promise<ArcGISIdentityManager> {
const auth = config.auth
const authType = config.auth?.type
Expand All @@ -79,7 +98,7 @@ export function getIdentityManager(
if (!factory) {
throw new Error(`No factory found for type ${authType}`)
}
return factory.create(getPortalUrl(config.url), getServerUrl(config.url), auth, httpClient)
return factory.create(getPortalUrl(config.url), getServerUrl(config.url), auth, processor)
}


Expand Down
14 changes: 12 additions & 2 deletions plugins/arcgis/service/src/ObservationProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { EventTransform } from './EventTransform';
import { GeometryChangedHandler } from './GeometryChangedHandler';
import { EventDeletionHandler } from './EventDeletionHandler';
import { EventLayerProcessorOrganizer } from './EventLayerProcessorOrganizer';
import { FeatureServiceConfig, FeatureLayerConfig, AuthType } from "./ArcGISConfig"
import { FeatureServiceConfig, FeatureLayerConfig, AuthType, OAuthAuthConfig } from "./ArcGISConfig"
import { PluginStateRepository } from '@ngageoint/mage.service/lib/plugins.api'
import { FeatureServiceAdmin } from './FeatureServiceAdmin';
import { HttpClient } from './HttpClient'
Expand Down Expand Up @@ -124,7 +124,9 @@ export class ObservationProcessor {
* @returns The current configuration from the database.
*/
public async safeGetConfig(): Promise<ArcGISPluginConfig> {
return await this._stateRepo.get().then(x => !!x ? x : this._stateRepo.put(defaultArcGISPluginConfig))
const state = await this._stateRepo.get();
if (!state) return await this._stateRepo.put(defaultArcGISPluginConfig);
return await this._stateRepo.get().then((state) => state ? state : this._stateRepo.put(defaultArcGISPluginConfig));
}

/**
Expand All @@ -135,6 +137,14 @@ export class ObservationProcessor {
return await this._stateRepo.put(newConfig);
}

/**
* Updates the confguration in the state repo.
* @param newConfig The new config to put into the state repo.
*/
public async patchConfig(newConfig: ArcGISPluginConfig): Promise<ArcGISPluginConfig> {
return await this._stateRepo.patch(newConfig);
}

/**
* Gets the current configuration and updates the processor if needed
* @returns The current configuration from the database.
Expand Down
34 changes: 22 additions & 12 deletions plugins/arcgis/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import { SettingPermission } from '@ngageoint/mage.service/lib/entities/authoriz
import { ArcGISPluginConfig } from './ArcGISPluginConfig'
import { AuthType } from './ArcGISConfig'
import { ObservationProcessor } from './ObservationProcessor'
import { HttpClient } from './HttpClient'
import { ArcGISIdentityManager, request } from "@esri/arcgis-rest-request"
import { FeatureServiceConfig } from './ArcGISConfig'
import { FeatureServiceConfig, OAuthAuthConfig } from './ArcGISConfig'
import { URL } from "node:url"
import express from 'express'
import { getIdentityManager, getPortalUrl } from './ArcGISIdentityManagerFactory'
Expand Down Expand Up @@ -38,6 +37,19 @@ const InjectedServices = {

const pluginWebRoute = "plugins/@ngageoint/mage.arcgis.service"

const sanitizeFeatureService = (config: FeatureServiceConfig, type: AuthType): FeatureServiceConfig => {
if (type === AuthType.OAuth) {
const newAuth = Object.assign({}, config.auth) as OAuthAuthConfig;
delete newAuth.refreshToken;
delete newAuth.refreshTokenExpires;
return {
...config,
auth: newAuth
}
}
return config;
}

/**
* The MAGE ArcGIS Plugin finds new MAGE observations and if configured to send the observations
* to an ArcGIS server, it will then transform the observation to an ArcGIS feature and
Expand Down Expand Up @@ -120,7 +132,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
}

await processor.putConfig(config)

// TODO: This seems like a bad idea to send the access tokens to the front end. It has no use for them and could potentially be a security concern
res.send(`
<html>
<head>
Expand Down Expand Up @@ -151,13 +163,14 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
.get(async (req, res, next) => {
console.info('Getting ArcGIS plugin config...')
const config = await processor.safeGetConfig()
config.featureServices = config.featureServices.map((service) => sanitizeFeatureService(service, AuthType.OAuth));
res.json(config)
})
.put(async (req, res, next) => {
console.info('Applying ArcGIS plugin config...')
const arcConfig = req.body as ArcGISPluginConfig
const configString = JSON.stringify(arcConfig)
processor.putConfig(arcConfig)
processor.patchConfig(arcConfig)
res.sendStatus(200)
})

Expand All @@ -179,18 +192,16 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
}

try {
const httpClient = new HttpClient(console)
// Create the IdentityManager instance to validate credentials
await getIdentityManager(service!, httpClient)
await getIdentityManager(service!, processor)
let existingService = config.featureServices.find(service => service.url === url)
if (existingService) {
existingService = { ...existingService }
} else {
config.featureServices.push(service)
}

await processor.putConfig(config)
return res.send(service)
await processor.patchConfig(config)
return res.send(sanitizeFeatureService(service, AuthType.OAuth))
} catch (err) {
return res.send('Invalid credentials provided to communicate with feature service').status(400)
}
Expand All @@ -203,10 +214,9 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
if (!featureService) {
return res.status(400)
}

const httpClient = new HttpClient(console)

try {
const identityManager = await getIdentityManager(featureService, httpClient)
const identityManager = await getIdentityManager(featureService, processor)
const response = await request(url, {
authentication: identityManager
})
Expand Down
10 changes: 0 additions & 10 deletions plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,6 @@ export interface OAuthAuthConfig {
* The expiration date for the temporary token
*/
authTokenExpires?: string

/**
* The Refresh token for OAuth
*/
refreshToken?: string

/**
* The expiration date for the Refresh token
*/
refreshTokenExpires?: string
}

/**
Expand Down
Loading

0 comments on commit c38a342

Please sign in to comment.