Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OAuth to ArcGIS service plugin #218

Merged
merged 7 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading