Skip to content

Commit af4a7c9

Browse files
authored
Update OAuth flow for ArcGIS Plugin (#225)
* initial commit * updates from pr comments * fix missing statements * move sanitize to web routes
1 parent ab77d6a commit af4a7c9

File tree

6 files changed

+82
-48
lines changed

6 files changed

+82
-48
lines changed

plugins/arcgis/service/src/ArcGISConfig.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,14 @@ export interface UsernamePasswordAuthConfig {
106106
* Contains OAuth authentication configuration.
107107
*/
108108
export interface OAuthAuthConfig {
109+
109110
type: AuthType.OAuth
111+
110112
/**
111113
* The Client Id for OAuth
112114
*/
113115
clientId: string
116+
114117
/**
115118
* The redirectUri for OAuth
116119
*/
@@ -121,7 +124,7 @@ export interface OAuthAuthConfig {
121124
*/
122125
authToken?: string
123126

124-
/**
127+
/**
125128
* The expiration date for the temporary token
126129
*/
127130
authTokenExpires?: number

plugins/arcgis/service/src/ArcGISIdentityManagerFactory.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"
1+
import { ArcGISIdentityManager, request } from "@esri/arcgis-rest-request"
22
import { ArcGISAuthConfig, AuthType, FeatureServiceConfig, OAuthAuthConfig, TokenAuthConfig, UsernamePasswordAuthConfig } from './ArcGISConfig'
3-
import { HttpClient } from "./HttpClient";
3+
import { ObservationProcessor } from "./ObservationProcessor";
44

55
interface ArcGISIdentityManagerFactory {
6-
create(portal: string, server: string, config: ArcGISAuthConfig, httpClient?: HttpClient): Promise<ArcGISIdentityManager>
6+
create(portal: string, server: string, config: ArcGISAuthConfig, processor?: ObservationProcessor): Promise<ArcGISIdentityManager>
77
}
88

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

@@ -22,14 +22,33 @@ const OAuthIdentityManagerFactory: ArcGISIdentityManagerFactory = {
2222
} else if (refreshToken && new Date(refreshTokenExpires || 0) > new Date()) {
2323
// TODO: find a way without using constructor nor httpClient
2424
const url = `${portal}/oauth2/token?client_id=${clientId}&refresh_token=${refreshToken}&grant_type=refresh_token`
25-
const response = await httpClient.sendGet(url)
26-
// TODO: error handling
27-
return ArcGISIdentityManager.fromToken({
28-
clientId: clientId,
29-
token: response.access_token,
30-
portal: portal
31-
});
32-
// TODO: update authToken to new token
25+
try {
26+
const response = await request(url, {
27+
httpMethod: 'GET'
28+
});
29+
30+
// Update authToken to new token
31+
const config = await processor.safeGetConfig();
32+
let service = config.featureServices.find(service => service.url === portal)?.auth as OAuthAuthConfig;
33+
const date = new Date();
34+
date.setSeconds(date.getSeconds() + response.expires_in || 0);
35+
service = {
36+
...service,
37+
authToken: response.access_token,
38+
authTokenExpires: date.getTime()
39+
}
40+
41+
await processor.putConfig(config)
42+
return ArcGISIdentityManager.fromToken({
43+
clientId: clientId,
44+
token: response.access_token,
45+
tokenExpires: date,
46+
portal: portal
47+
});
48+
} catch (error) {
49+
throw new Error('Error occurred when using refresh token')
50+
}
51+
3352
} else {
3453
// TODO the config, we need to let the user know UI side they need to authenticate again
3554
throw new Error('Refresh token missing or expired')
@@ -46,7 +65,7 @@ const TokenIdentityManagerFactory: ArcGISIdentityManagerFactory = {
4665
server: server,
4766
// TODO: what do we really want to do here? esri package seems to need this optional parameter.
4867
// Use authTokenExpires if defined, otherwise set to now plus a day
49-
tokenExpires: auth.authTokenExpires ? new Date(auth.authTokenExpires) : new Date(Date.now() + 24 * 60 * 60 * 1000)
68+
tokenExpires: auth.authTokenExpires ? new Date(auth.authTokenExpires) : new Date(Date.now() + 24 * 60 * 60 * 1000)
5069
})
5170
return identityManager
5271
}
@@ -68,7 +87,7 @@ const authConfigMap: { [key: string]: ArcGISIdentityManagerFactory } = {
6887

6988
export function getIdentityManager(
7089
config: FeatureServiceConfig,
71-
httpClient: HttpClient // TODO remove in favor of an open source lib like axios
90+
processor: ObservationProcessor
7291
): Promise<ArcGISIdentityManager> {
7392
const auth = config.auth
7493
const authType = config.auth?.type
@@ -79,7 +98,7 @@ export function getIdentityManager(
7998
if (!factory) {
8099
throw new Error(`No factory found for type ${authType}`)
81100
}
82-
return factory.create(getPortalUrl(config.url), getServerUrl(config.url), auth, httpClient)
101+
return factory.create(getPortalUrl(config.url), getServerUrl(config.url), auth, processor)
83102
}
84103

85104

plugins/arcgis/service/src/ObservationProcessor.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { EventTransform } from './EventTransform';
1414
import { GeometryChangedHandler } from './GeometryChangedHandler';
1515
import { EventDeletionHandler } from './EventDeletionHandler';
1616
import { EventLayerProcessorOrganizer } from './EventLayerProcessorOrganizer';
17-
import { FeatureServiceConfig, FeatureLayerConfig, AuthType } from "./ArcGISConfig"
17+
import { FeatureServiceConfig, FeatureLayerConfig, AuthType, OAuthAuthConfig } from "./ArcGISConfig"
1818
import { PluginStateRepository } from '@ngageoint/mage.service/lib/plugins.api'
1919
import { FeatureServiceAdmin } from './FeatureServiceAdmin';
2020

@@ -121,7 +121,9 @@ export class ObservationProcessor {
121121
* @returns The current configuration from the database.
122122
*/
123123
public async safeGetConfig(): Promise<ArcGISPluginConfig> {
124-
return await this._stateRepo.get().then(x => !!x ? x : this._stateRepo.put(defaultArcGISPluginConfig))
124+
const state = await this._stateRepo.get();
125+
if (!state) return await this._stateRepo.put(defaultArcGISPluginConfig);
126+
return await this._stateRepo.get().then((state) => state ? state : this._stateRepo.put(defaultArcGISPluginConfig));
125127
}
126128

127129
/**
@@ -132,6 +134,14 @@ export class ObservationProcessor {
132134
return await this._stateRepo.put(newConfig);
133135
}
134136

137+
/**
138+
* Updates the confguration in the state repo.
139+
* @param newConfig The new config to put into the state repo.
140+
*/
141+
public async patchConfig(newConfig: ArcGISPluginConfig): Promise<ArcGISPluginConfig> {
142+
return await this._stateRepo.patch(newConfig);
143+
}
144+
135145
/**
136146
* Gets the current configuration and updates the processor if needed
137147
* @returns The current configuration from the database.

plugins/arcgis/service/src/index.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import { SettingPermission } from '@ngageoint/mage.service/lib/entities/authoriz
77
import { ArcGISPluginConfig } from './ArcGISPluginConfig'
88
import { AuthType } from './ArcGISConfig'
99
import { ObservationProcessor } from './ObservationProcessor'
10-
import { HttpClient } from './HttpClient'
1110
import { ArcGISIdentityManager, request } from "@esri/arcgis-rest-request"
12-
import { FeatureServiceConfig } from './ArcGISConfig'
11+
import { FeatureServiceConfig, OAuthAuthConfig } from './ArcGISConfig'
1312
import { URL } from "node:url"
1413
import express from 'express'
1514
import { getIdentityManager, getPortalUrl } from './ArcGISIdentityManagerFactory'
@@ -38,6 +37,19 @@ const InjectedServices = {
3837

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

40+
const sanitizeFeatureService = (config: FeatureServiceConfig, type: AuthType): FeatureServiceConfig => {
41+
if (type === AuthType.OAuth) {
42+
const newAuth = Object.assign({}, config.auth) as OAuthAuthConfig;
43+
delete newAuth.refreshToken;
44+
delete newAuth.refreshTokenExpires;
45+
return {
46+
...config,
47+
auth: newAuth
48+
}
49+
}
50+
return config;
51+
}
52+
4153
/**
4254
* The MAGE ArcGIS Plugin finds new MAGE observations and if configured to send the observations
4355
* to an ArcGIS server, it will then transform the observation to an ArcGIS feature and
@@ -120,7 +132,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
120132
}
121133

122134
await processor.putConfig(config)
123-
135+
// 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
124136
res.send(`
125137
<html>
126138
<head>
@@ -151,13 +163,14 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
151163
.get(async (req, res, next) => {
152164
console.info('Getting ArcGIS plugin config...')
153165
const config = await processor.safeGetConfig()
166+
config.featureServices = config.featureServices.map((service) => sanitizeFeatureService(service, AuthType.OAuth));
154167
res.json(config)
155168
})
156169
.put(async (req, res, next) => {
157170
console.info('Applying ArcGIS plugin config...')
158171
const arcConfig = req.body as ArcGISPluginConfig
159172
const configString = JSON.stringify(arcConfig)
160-
processor.putConfig(arcConfig)
173+
processor.patchConfig(arcConfig)
161174
res.sendStatus(200)
162175
})
163176

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

181194
try {
182-
const httpClient = new HttpClient(console)
183195
// Create the IdentityManager instance to validate credentials
184-
await getIdentityManager(service!, httpClient)
196+
await getIdentityManager(service!, processor)
185197
let existingService = config.featureServices.find(service => service.url === url)
186198
if (existingService) {
187199
existingService = { ...existingService }
188200
} else {
189201
config.featureServices.push(service)
190202
}
191-
192-
await processor.putConfig(config)
193-
return res.send(service)
203+
await processor.patchConfig(config)
204+
return res.send(sanitizeFeatureService(service, AuthType.OAuth))
194205
} catch (err) {
195206
return res.send('Invalid credentials provided to communicate with feature service').status(400)
196207
}
@@ -203,10 +214,9 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
203214
if (!featureService) {
204215
return res.status(400)
205216
}
206-
207-
const httpClient = new HttpClient(console)
217+
208218
try {
209-
const identityManager = await getIdentityManager(featureService, httpClient)
219+
const identityManager = await getIdentityManager(featureService, processor)
210220
const response = await request(url, {
211221
authentication: identityManager
212222
})

plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,6 @@ export interface OAuthAuthConfig {
126126
* The expiration date for the temporary token
127127
*/
128128
authTokenExpires?: string
129-
130-
/**
131-
* The Refresh token for OAuth
132-
*/
133-
refreshToken?: string
134-
135-
/**
136-
* The expiration date for the Refresh token
137-
*/
138-
refreshTokenExpires?: string
139129
}
140130

141131
/**

plugins/arcgis/web-app/projects/main/src/lib/arc.service.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,17 @@ export class ArcService implements ArcServiceInterface {
6161
const oauthWindow = window.open(url, "_blank");
6262

6363
const listener = (event: any) => {
64-
window.removeEventListener('message', listener, false);
65-
66-
if (event.origin !== window.location.origin) {
67-
subject.error('target origin mismatch')
64+
if (event.data.url) {
65+
window.removeEventListener('message', listener, false);
66+
67+
if (event.origin !== window.location.origin) {
68+
subject.error('target origin mismatch')
69+
}
70+
71+
subject.next(event.data)
72+
73+
oauthWindow?.close();
6874
}
69-
70-
subject.next(event.data)
71-
72-
oauthWindow?.close();
7375
}
7476

7577
window.addEventListener('message', listener, false);

0 commit comments

Comments
 (0)