Skip to content

Commit

Permalink
Add support for user OAuth (#219)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanslatten authored Oct 1, 2024
1 parent de2c5ab commit 20b86de
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 31 deletions.
24 changes: 22 additions & 2 deletions plugins/arcgis/service/src/ArcGISConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,29 @@ export interface ArcGISAuthConfig {
clientId?: string

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

/**
* The temporary auth token for OAuth
*/
authToken?: string

/**
* 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
16 changes: 10 additions & 6 deletions plugins/arcgis/service/src/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,16 @@ export class HttpClient {
*/
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;
return new Promise((resolve, reject) => {
try {
this.sendGetHandleResponse(url, function (chunk) {
console.log('Response: ' + chunk);
resolve(JSON.parse(chunk));
})
} catch (error) {
reject(error)
}
});
}

/**
Expand Down
4 changes: 2 additions & 2 deletions plugins/arcgis/service/src/ObservationProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export class ObservationProcessor {
* Puts a new confguration in the state repo.
* @param newConfig The new config to put into the state repo.
*/
public putConfig(newConfig: ArcGISPluginConfig) {
this._stateRepo.put(newConfig);
public async putConfig(newConfig: ArcGISPluginConfig): Promise<ArcGISPluginConfig> {
return await this._stateRepo.put(newConfig);
}

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

// TODO: Remove hard coded creds
const credentials = {
clientId: 'kbHGOg5BFjYf1sTA',
portal: 'https://arcgis.geointnext.com/arcgis/sharing/rest',
redirectUri: 'http://localhost:4242/plugins/@ngageoint/mage.arcgis.service/oauth/authenticate'
}

const logPrefix = '[mage.arcgis]'
const logMethods = ['log', 'debug', 'info', 'warn', 'error'] as const
const consoleOverrides = logMethods.reduce((overrides, fn) => {
Expand Down Expand Up @@ -64,11 +71,10 @@ 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, httpClient: HttpClient): Promise<ArcGISIdentityManager> {
async function handleAuthentication(req: express.Request, httpClient: HttpClient, processor: ObservationProcessor): 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;
Expand All @@ -79,30 +85,51 @@ async function handleAuthentication(req: express.Request, httpClient: HttpClient
try {
if (featureToken) {
console.log('Token provided for authentication');
identityManager = await ArcGISIdentityManager.fromToken({ token: featureToken, server: getServerUrl(req.query.featureUrl as string ?? ''), portal: portalUrl });
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: 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
}
} else if (featureClientId) {
console.log('Client ID provided for authentication');

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
});
// Check if feature service has refresh token and use that to generate token to use
// Else complain
const config = await processor.safeGetConfig();
const featureService = config.featureServices.find((service) => service.auth?.clientId === featureClientId);
const authToken = featureService?.auth?.authToken;
const authTokenExpires = featureService?.auth?.authTokenExpires as string;
if (authToken && new Date(authTokenExpires) > new Date()) {
// TODO: error handling
identityManager = await ArcGISIdentityManager.fromToken({
clientId: featureClientId,
token: authToken,
portal: portalUrl
});
} else {
const refreshToken = featureService?.auth?.refreshToken;
const refreshTokenExpires = featureService?.auth?.refreshTokenExpires as string;
if (refreshToken && new Date(refreshTokenExpires) > new Date()) {
const url = `${portalUrl}/oauth2/token?client_id=${featureClientId}&refresh_token=${refreshToken}&grant_type=refresh_token`
const response = await httpClient.sendGet(url)
// TODO: error handling
identityManager = await ArcGISIdentityManager.fromToken({
clientId: featureClientId,
token: response.access_token,
portal: portalUrl
});
// TODO: update authToken to new token
} else {
throw new Error('Refresh token missing or expired.')
}
}
} else {
throw new Error('Missing required query parameters to authenticate (token or username/password).');
}
Expand Down Expand Up @@ -144,7 +171,51 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
webRoutes: {
public: (requestContext: GetAppRequestContext) => {
const routes = express.Router().use(express.json());
// TODO: Add User initiated Oauth
routes.get("/oauth/sign-in", async (req, res) => {
const clientId = req.query.clientId as string;
const portal = req.query.portalUrl as string;
const redirectUri = req.query.redirectUrl as string;
// TODO: Replace with better way if possible to pass creds to /oauth/authenticate
const config = await processor.safeGetConfig();
config.featureServices.push({
url: portal,
layers: [],
auth: {
clientId: clientId,
redirectUri: redirectUri
}
})
await processor.putConfig(config);
ArcGISIdentityManager.authorize({
clientId,
portal,
redirectUri
}, res);
})
routes.get('/oauth/authenticate', async (req, res) => {
const code = req.query.code as string;
// TODO: Use req or session data to find correct feature service instead of hard coding
// TODO: error handling
const config = await processor.safeGetConfig();
const featureService = config.featureServices[0];
const creds = {
clientId: featureService.auth?.clientId as string,
redirectUri: featureService.auth?.redirectUri as string,
portal: featureService.url as string
}
ArcGISIdentityManager.exchangeAuthorizationCode(creds, code)
.then(async (idManager: ArcGISIdentityManager) => {
featureService.auth = {
...featureService.auth,
authToken: idManager.token,
authTokenExpires: idManager.tokenExpires.toISOString(),
refreshToken: idManager.refreshToken,
refreshTokenExpires: idManager.refreshTokenExpires.toISOString()
}
await processor.putConfig(config);
res.status(200).json({})
}).catch((error) => res.status(400).json(error))
})
return routes
},
protected: (requestContext: GetAppRequestContext) => {
Expand Down Expand Up @@ -180,7 +251,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
const httpClient = new HttpClient(console);

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

const featureUrlAndToken = featureUrl + '?token=' + encodeURIComponent(identityManager.token);
console.log('featureUrlAndToken', featureUrlAndToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class ArcService {
}

subject.next(event.data)

// TODO: Fix window to send data
// authWindow?.close();
}

Expand Down

0 comments on commit 20b86de

Please sign in to comment.