Skip to content

Commit

Permalink
Add OAuth to ArcGIS service plugin (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanslatten authored Sep 30, 2024
1 parent d4901e8 commit de2c5ab
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 53 deletions.
14 changes: 12 additions & 2 deletions plugins/arcgis/service/src/ArcGISConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,22 @@ export interface ArcGISAuthConfig {
/**
* The username for authentication.
*/
username: string
username?: string

/**
* The password for authentication.
*/
password: string
password?: string

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

/**
* The Client secret for OAuth
*/
clientSecret?: string
}

/**
Expand Down
5 changes: 4 additions & 1 deletion plugins/arcgis/service/src/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,14 @@ export class HttpClient {
* Sends a get request to the specified url.
* @param url The url of the get request.
*/
sendGet(url: string) {
async sendGet(url: string): Promise<any> {
const console = this._console
let response;
this.sendGetHandleResponse(url, function (chunk) {
console.log('Response: ' + chunk);
response = chunk;
})
return response;
}

/**
Expand Down
56 changes: 34 additions & 22 deletions plugins/arcgis/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { FeatureServiceResult } from './FeatureServiceResult'
import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"
// import { IQueryFeaturesOptions, queryFeatures } from '@esri/arcgis-rest-feature-service'


const logPrefix = '[mage.arcgis]'
const logMethods = ['log', 'debug', 'info', 'warn', 'error'] as const
const consoleOverrides = logMethods.reduce((overrides, fn) => {
Expand Down Expand Up @@ -65,25 +64,44 @@ function getServerUrl(featureServiceUrl: string): string {
*
* @throws {Error} If the identity manager could not be created due to missing required query parameters.
*/
async function handleAuthentication(req: express.Request): Promise<ArcGISIdentityManager> {
async function handleAuthentication(req: express.Request, httpClient: HttpClient): Promise<ArcGISIdentityManager> {
const featureUsername = req.query.username as string | undefined;
const featurePassword = req.query.password as string | undefined;
const featureClientId = req.query.clientId as string | undefined;
const featureClientSecret = req.query.clientSecret as string | undefined;
const featureServer = req.query.server as string | undefined;
const featurePortal = req.query.portal as string | undefined;
const featureToken = req.query.token as string | undefined;
const portalUrl = getPortalUrl(req.query.featureUrl as string ?? '');

let identityManager: ArcGISIdentityManager;

try {
if (featureToken) {
console.log('Token provided for authentication');
identityManager = await ArcGISIdentityManager.fromToken({ token: featureToken, server: getServerUrl(req.query.featureUrl as string ?? ''), portal: getPortalUrl(req.query.featureUrl as string ?? '') });
identityManager = await ArcGISIdentityManager.fromToken({ token: featureToken, server: getServerUrl(req.query.featureUrl as string ?? ''), portal: portalUrl });
} else if (featureUsername && featurePassword) {
console.log('Username and password provided for authentication, username:' + featureUsername);
identityManager = await ArcGISIdentityManager.signIn({
username: featureUsername,
password: featurePassword,
portal: getPortalUrl(req.query.featureUrl as string ?? ''),
portal: portalUrl,
});
} else if (featureClientId && featureClientSecret) {
console.log('ClientId and Client secret provided for authentication');
const params = {
client_id: featureClientId,
client_secret: featureClientSecret,
grant_type: 'client_credentials',
expiration: 900
}

const url = `${portalUrl}/oauth2/token?client_id=${params.client_id}&client_secret=${params.client_secret}&grant_type=${params.grant_type}&expiration=${params.expiration}`
const response = await httpClient.sendGet(url);
identityManager = await ArcGISIdentityManager.fromToken({
clientId: featureClientId,
token: JSON.parse(response)?.access_token || '',
portal: portalUrl
});
} else {
throw new Error('Missing required query parameters to authenticate (token or username/password).');
Expand Down Expand Up @@ -118,21 +136,15 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
// - Move getPortalUrl to Helper file
// - Update layer token to get token from identity manager
// - Move plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts addLayer to helper file and use instead of encodeURIComponent
// - Remove Client secret from returned Config object if applicable

const processor = new ObservationProcessor(stateRepo, eventRepo, obsRepoForEvent, userRepo, console);
processor.start();
return {
webRoutes: {
public: (requestContext: GetAppRequestContext) => {
const routes = express.Router().use(express.json())
routes.post('/oauth/signin', async (req, res, next) => {
// TODO implement
})

routes.post('/oauth/authenticate', async (req, res, next) => {
// TODO implement
})

const routes = express.Router().use(express.json());
// TODO: Add User initiated Oauth
return routes
},
protected: (requestContext: GetAppRequestContext) => {
Expand Down Expand Up @@ -165,15 +177,15 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
const featureUrl = req.query.featureUrl as string;
console.info('Getting ArcGIS layer info for ' + featureUrl)
let identityManager: ArcGISIdentityManager;
const httpClient = new HttpClient(console);

try {
identityManager = await handleAuthentication(req);
try {
identityManager = await handleAuthentication(req, httpClient);

const featureUrlAndToken = featureUrl + '?token=' + encodeURIComponent(identityManager.token);
console.log('featureUrlAndToken', featureUrlAndToken);
const httpClient = new HttpClient(console);

httpClient.sendGetHandleResponse(featureUrlAndToken, (chunk) => {
const featureUrlAndToken = featureUrl + '?token=' + encodeURIComponent(identityManager.token);
console.log('featureUrlAndToken', featureUrlAndToken);

httpClient.sendGetHandleResponse(featureUrlAndToken, (chunk) => {
console.info('ArcGIS layer info response ' + chunk);
try {
const featureServiceResult = JSON.parse(chunk) as FeatureServiceResult;
Expand All @@ -188,8 +200,8 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
}
});
} catch (err) {
res.status(500).json({ message: 'Could not get ArcGIS layer info', error: err });
}
res.status(500).json({ message: 'Could not get ArcGIS layer info', error: err });
}
})

return routes
Expand Down
18 changes: 15 additions & 3 deletions plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface FeatureServiceConfig {
/**
* Access token
*/
token?: string
token?: string // TODO?: Perhaps move to the auth property?

/**
* Username and password for ArcGIS authentication
Expand Down Expand Up @@ -82,15 +82,27 @@ export interface FeatureLayerConfig {
*/
export interface ArcGISAuthConfig {

// TODO?: May want to add authType property

/**
* The username for authentication.
*/
username: string
username?: string

/**
* The password for authentication.
*/
password: string
password?: string

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

/**
* The Client secret for OAuth
*/
clientSecret?: string
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ <h2>Feature Layers</h2>
<button mat-button mat-stroked-button color="primary" aria-label="Add ArcGIS feature layer"
(click)="onAddLayer()">ADD FEATURE SERVICE</button>
</div>
<div>
<button mat-button mat-stroked-button color="primary" aria-label="Sign in with OAuth"
(click)="onSignIn()">Sign in with OAuth</button>
</div>
<div class="arcLayers" *ngIf="!config.featureServices.length">
<div class="arcLayer">
There are no ArcGIS feature services currently being synchronized.
Expand Down Expand Up @@ -70,7 +74,7 @@ <h3 matDialogTitle>Layers</h3>
<mat-dialog-actions align="end">
<button mat-button matDialogClose>CANCEL</button>
<button [disabled]="isSaveDisabled()" mat-flat-button color="primary" matDialogClose
(click)="onAddLayerUrl(layerUrl.value, layerToken.value, layers)">SAVE</button>
(click)="onAddLayerUrl({ layerUrl: layerUrl.value, selectableLayers: layers, layerToken: layerToken.value })">SAVE</button>
</mat-dialog-actions>
</ng-template>
<ng-template #deleteLayerDialog let-data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class ArcLayerComponent implements OnInit {
isLoading: boolean;
currentUrl?: string;
private timeoutId: number;

@ViewChild('addLayerDialog', { static: true })
private addLayerTemplate: TemplateRef<unknown>
@ViewChild('deleteLayerDialog', { static: true })
Expand Down Expand Up @@ -123,6 +123,20 @@ export class ArcLayerComponent implements OnInit {
})
}

onSignIn() {
this.arcService.authenticate().subscribe({
next(x) {
console.log('got value ' + JSON.stringify(x));
},
error(err) {
console.error('something wrong occurred: ' + err);
},
complete() {
console.log('done');
},
});
}

onAddLayer() {
this.currentUrl = undefined
this.arcLayerControl.setValue('')
Expand Down Expand Up @@ -151,12 +165,6 @@ export class ArcLayerComponent implements OnInit {
this.arcService.putArcConfig(this.config);
}


// Define the overloads
onAddLayerUrl(layerUrl: string, layerToken: string, layers: ArcLayerSelectable[]): void;
onAddLayerUrl(layerUrl: string, username: string, password: string, layers: ArcLayerSelectable[]): void;

// Implement the function
/**
* Adds a new layer to the configuration if it does not already exist.
*
Expand All @@ -174,43 +182,48 @@ export class ArcLayerComponent implements OnInit {
* 6. Updates the configuration and emits the change.
* 7. Persists the updated configuration using `arcService`.
*/
onAddLayerUrl(layerUrl: string, arg2: string, arg3: string | ArcLayerSelectable[], arg4?: ArcLayerSelectable[]): void {
onAddLayerUrl(params: {
layerUrl: string,
selectableLayers: ArcLayerSelectable[],
layerToken?: string,
username?: string,
password?: string,
clientId?: string,
clientSecret?: string
}): void {
let serviceConfigToEdit = null;
const { layerUrl, selectableLayers, layerToken, username, password, clientId, clientSecret } = params;

// Search if the layer in config to edit
for (const service of this.config.featureServices) {
if (service.url == layerUrl) {
serviceConfigToEdit = service;
}
}
// Determine if layers in 3rd or 4th argument
const layers = typeof arg3 === 'string' ? arg4 : arg3;

// Add layer if it doesn't exist
if (serviceConfigToEdit == null) {
console.log('Adding layer ' + layerUrl);
let token: string | null = null;

const featureLayer: FeatureServiceConfig = {
url: layerUrl,
token: undefined,
auth: {
username: '',
password: ''
},
auth: {},
layers: []
} as FeatureServiceConfig;

if (typeof arg3 === 'string') {
if (username) {
// Handle username and password case
featureLayer.auth = { username: arg2, password: arg3 };
} else {
featureLayer.auth = { username, password };
} else if (clientId) {
featureLayer.auth = { clientId, clientSecret };
}else {
// Handle token case
featureLayer.token = arg2;
featureLayer.token = layerToken;
}

if (layers) {
for (const aLayer of layers) {
if (selectableLayers) {
for (const aLayer of selectableLayers) {
if (aLayer.isSelected) {
const layerConfig = {
layer: aLayer.name,
Expand All @@ -229,8 +242,8 @@ export class ArcLayerComponent implements OnInit {
} else { // Edit existing layer
console.log('Saving edited layer ' + layerUrl)
const editedLayers = [];
if (layers) {
for (const aLayer of layers) {
if (selectableLayers) {
for (const aLayer of selectableLayers) {
if (aLayer.isSelected) {
let layerConfig = null
if (serviceConfigToEdit.layers != null) {
Expand Down
25 changes: 24 additions & 1 deletion plugins/arcgis/web-app/projects/main/src/lib/arc.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { Observable, Subject } from 'rxjs'
import { ArcGISPluginConfig } from './ArcGISPluginConfig'
import { FeatureServiceResult } from './FeatureServiceResult'
import { EventResult } from './EventsResult'
Expand Down Expand Up @@ -30,6 +30,29 @@ export class ArcService {
return this.http.get<FeatureServiceResult>(`${baseUrl}/arcgisLayers?featureUrl=${featureUrl}`)
}

authenticate(): Observable<any> {
let subject = new Subject<any>();

const url = `${baseUrl}/oauth/sign-in`;
const authWindow = window.open(url, "_blank");

function onMessage(event: any) {
window.removeEventListener('message', onMessage, false);

if (event.origin !== window.location.origin) {
return;
}

subject.next(event.data)

// authWindow?.close();
}

authWindow?.addEventListener('message', onMessage, false);

return subject.asObservable()
}

fetchEvents() {
return this.http.get<EventResult[]>(`${apiBaseUrl}/events?populate=false&projection={"name":true,"id":true}`)
}
Expand Down

0 comments on commit de2c5ab

Please sign in to comment.