Skip to content

Commit

Permalink
Authentication with Google Account (experimental)
Browse files Browse the repository at this point in the history
  • Loading branch information
m-mohr committed Jan 19, 2024
1 parent bb7f1f9 commit 8d435fb
Show file tree
Hide file tree
Showing 27 changed files with 287 additions and 143 deletions.
2 changes: 0 additions & 2 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ extends:
parserOptions:
ecmaVersion: 2022
sourceType: module
globals:
ee: readonly
rules:
n/no-extraneous-import:
- error
Expand Down
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,46 @@ There are several important configuration options in the file [config.json](conf

#### Setting up GEE authentication

Generally, information about authentication with Google Earth Engine can be found in the [Earth Engine documentation](https://developers.google.com/earth-engine/app_engine_intro).

##### Service Account

If you want to run all processing through a single account you can use service accounts. That's the most reliable way right now.
The server needs to authenticate with a [service accounts](https://developers.google.com/earth-engine/service_account) using a private key. The account need to have access rights for earth engine. You need to drop your private key file into a secure place specify the file path of the private key in the property `serviceAccountCredentialsFile` in the file [config.json](config.json).

More information about authentication can be found in the [Earth Engine documentation](https://developers.google.com/earth-engine/app_engine_intro).
##### Google User Accounts

**EXPERIMENTAL:** *This authentication method currently requires you to login every 60 minutes unless the
openEO clients refresh the tokens automatically. User workspaces also don't work reliably as of now.*

Alternatively, you can configure the driver to let users authenticatie with their User Accounts via OAuth2 / OpenID Connect.
For this you need to configure the property `googleAuthClients` in the file [config.json](config.json).

You want to have at least client IDs for (1) "Web Application" and (2) "TVs & limited-input devices" from the
[Google Cloud Console](https://console.cloud.google.com/apis/credentials).

For example:

```json
[
{
"id": "1234567890-abcdefghijklmnop.apps.googleusercontent.com",
"grant_types": [
"implicit"
],
"redirect_urls": [
"https://editor.openeo.org/",
"http://localhost/"
]
},
{
"id": "0123456789-abcdefghijklmnop.apps.googleusercontent.com",
"grant_types": [
"urn:ietf:params:oauth:grant-type:device_code+pkce"
]
}
]
```

### Starting up the server

Expand Down
6 changes: 3 additions & 3 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
"key": null,
"certificate": null
},
"serviceAccountCredentialsFile": "privatekey.json",
"googleProjectId": "",
"serviceAccountCredentialsFile": "",
"id": "openeo-earthengine-driver",
"title": "Google Earth Engine Proxy for openEO",
"description": "This is the Google Earth Engine Driver for openEO.\n\nGoogle Earth Engine is a planetary-scale platform for Earth science data & analysis. It is powered by Google's cloud infrastructure and combines a multi-petabyte catalog of satellite imagery and geospatial datasets with planetary-scale analysis capabilities. Google makes it available for scientists, researchers, and developers to detect changes, map trends, and quantify differences on the Earth's surface. Google Earth Engine is free for research, education, and nonprofit use.",
Expand All @@ -26,5 +25,6 @@
}
]
},
"otherVersions": []
"otherVersions": [],
"googleAuthClients": []
}
33 changes: 28 additions & 5 deletions src/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export default class UsersAPI {

beforeServerStart(server) {
server.addEndpoint('get', '/credentials/basic', this.getCredentialsBasic.bind(this));
// server.addEndpoint('get', '/credentials/oidc', this.getCredentialsOidc.bind(this));
if (this.context.googleAuthClients) {
server.addEndpoint('get', '/credentials/oidc', this.getCredentialsOidc.bind(this));
}
server.addEndpoint('get', '/me', this.getUserInfo.bind(this));

return Promise.resolve();
Expand All @@ -28,13 +30,34 @@ export default class UsersAPI {
try {
req.user = await this.storage.checkAuthToken(token);
} catch(err) {
res.send(Error.wrap(err));
res.send(Errors.wrap(err));
}
}

// getCredentialsOidc(req, res, next) {
// res.redirect('https://accounts.google.com/.well-known/openid-configuration', next);
// }
async getCredentialsOidc(req, res) {
if (!this.context.googleAuthClients) {
throw new Errors.FeatureUnsupported();
}

res.send({
"providers": [
{
id: "google",
issuer: "https://accounts.google.com",
title: "Google",
description: "Login with your Google Earth Engine account.",
scopes: [
"openid",
"email",
"https://www.googleapis.com/auth/earthengine",
// "https://www.googleapis.com/auth/cloud-platform",
// "https://www.googleapis.com/auth/devstorage.full_control"
],
default_clients: this.context.googleAuthClients
}
]
});
}

async getCredentialsBasic(req, res) {
if (!req.authorization.scheme) {
Expand Down
3 changes: 1 addition & 2 deletions src/models/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ export default class DataCatalog {
}

const storage = new Storage({
keyFile: './privatekey.json',
projectId: this.serverContext.googleProjectId
keyFile: './privatekey.json'
});
const bucket = storage.bucket('earthengine-stac');
const prefix = 'catalog/';
Expand Down
43 changes: 40 additions & 3 deletions src/models/userstore.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ export default class UserStore {
return await this.db.insertAsync(userData);
}

async checkAuthToken(token) {
async authenticateBasic(token) {
const query = {
token: token.replace(/^basic\/\//, ''), // remove token prefix for basic
token,
validity: { $gt: Utils.getTimestamp() }
};

Expand All @@ -120,8 +120,45 @@ export default class UserStore {
reason: 'User account has been removed.'
});
}

return user;
}

async authenticateGoogle(token) {
const userData = this.emptyUser(false);
userData._id = "google-" + Utils.generateHash(8);
userData.token = token;
// Googles tokens are valid for roughly an hour, so we set it slightly lower
userData.token_valid_until = Utils.getTimestamp() + 59 * 60;
return userData;
}

async checkAuthToken(apiToken) {
const parts = apiToken.split('/', 3);
if (parts.length !== 3) {
throw new Errors.AuthenticationRequired({
reason: 'Token format invalid.'
});
}
const [type, provider, token] = parts;

if (type === 'basic') {
return this.authenticateBasic(token);
}
else if (type === 'oidc') {
if (provider === 'google') {
return this.authenticateGoogle(token);
}
else {
throw new Errors.AuthenticationRequired({
reason: 'Identity provider not supported.'
});
}
}
else {
throw new Errors.AuthenticationRequired({
reason: 'Authentication method not supported.'
});
}
}

}
3 changes: 2 additions & 1 deletion src/processes/aggregate_temporal_frequency.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ export default class aggregate_temporal_frequency extends BaseProcess {
}

async execute(node) {
const ee = node.ee;
// STEP 1: Get parameters and set some variables
const dc = node.getDataCube('data');
const frequency = node.getArgument('frequency');

// STEP 2: prepare image collection with aggregation label
const images = Commons.setAggregationLabels(dc.imageCollection(), frequency);
const images = Commons.setAggregationLabels(node, dc.imageCollection(), frequency);

// STEP 3: aggregate based on aggregation label

Expand Down
4 changes: 2 additions & 2 deletions src/processes/anomaly.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ export default class anomaly extends BaseProcess {
async execute(node) {
const dc = node.getDataCube('data');
const normalsDataCube = node.getDataCube('normals');
const normalsLabels = ee.List(normalsDataCube.dimT().getValues());
const normalsLabels = node.ee.List(normalsDataCube.dimT().getValues());
const normalsCollection = normalsDataCube.imageCollection();
const normals = normalsCollection.toList(normalsCollection.size());
const frequency = node.getArgument('frequency');

let images = Commons.setAggregationLabels(dc.imageCollection(), frequency);
let images = Commons.setAggregationLabels(node, dc.imageCollection(), frequency);
images = images.map(image => {
const label = image.get('label');
const normal = normals.get(normalsLabels.indexOf(label));
Expand Down
6 changes: 4 additions & 2 deletions src/processes/climatological_normal.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { BaseProcess } from '@openeo/js-processgraphs';
import Commons from '../processgraph/commons.js';
import Utils from '../utils/utils.js';
import GeeUtils from '../processgraph/utils.js';

export default class climatological_normal extends BaseProcess {

async execute(node) {
const ee = node.ee;
const dc = node.getDataCube('data');
const frequency = node.getArgument('frequency');

Expand All @@ -25,7 +27,7 @@ export default class climatological_normal extends BaseProcess {
break;
case 'seasons':
// Define seasons + labels
seasons = Utils.seasons();
seasons = GeeUtils.seasons(node);
geeSeasons = ee.Dictionary(seasons);
labels = Object.keys(seasons);
range = geeSeasons.values();
Expand All @@ -41,7 +43,7 @@ export default class climatological_normal extends BaseProcess {
break;
case 'tropical_seasons':
// Define seasons + labels
seasons = Utils.tropicalSeasons();
seasons = GeeUtils.tropicalSeasons(node);
geeSeasons = ee.Dictionary(seasons);
labels = Object.keys(seasons);
range = geeSeasons.values();
Expand Down
2 changes: 1 addition & 1 deletion src/processes/create_raster_cube.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import DataCube from '../processgraph/datacube.js';
export default class create_raster_cube extends BaseProcess {

async execute(node) {
const dc = new DataCube();
const dc = new DataCube(node.ee);
dc.setLogger(node.getLogger());
return dc;
}
Expand Down
2 changes: 1 addition & 1 deletion src/processes/filter_bbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Commons from '../processgraph/commons.js';
export default class filter_bbox extends BaseProcess {

async execute(node) {
return Commons.filterBbox(node.getDataCube("data"), node.getArgument("extent"), this.id, 'extent');
return Commons.filterBbox(node, node.getDataCube("data"), node.getArgument("extent"), this.id, 'extent');
}

}
2 changes: 1 addition & 1 deletion src/processes/filter_spatial.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Commons from '../processgraph/commons.js';
export default class filter_spatial extends BaseProcess {

async execute(node) {
return Commons.filterGeoJSON(node.getData("data"), node.getArgument("geometries"), this.id, 'geometries');
return Commons.filterGeoJSON(node, node.getData("data"), node.getArgument("geometries"), this.id, 'geometries');
}

}
1 change: 1 addition & 0 deletions src/processes/first.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default class first extends BaseProcess {
}

async execute(node) {
const ee = node.ee;
const data = node.getArgument('data');

if (Array.isArray(data)) {
Expand Down
2 changes: 1 addition & 1 deletion src/processes/if.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default class If extends BaseProcess {
const accept = node.getArgument('accept');
const reject = node.getArgument('reject');

return ee.Algorithms.If(value, accept, reject);
return node.ee.Algorithms.If(value, accept, reject);
//if (value === true) {
// return accept;
//}
Expand Down
1 change: 1 addition & 0 deletions src/processes/last.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default class last extends BaseProcess {
}

async execute(node) {
const ee = node.ee;
const data = node.getArgument('data');

if (Array.isArray(data)) {
Expand Down
9 changes: 5 additions & 4 deletions src/processes/load_collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import Commons from '../processgraph/commons.js';
export default class load_collection extends BaseProcess {

async execute(node) {
const ee = node.ee;
// Load data
const id = node.getArgument('id');
const collection = node.getContext().getCollection(id);
let dc = new DataCube();
let dc = new DataCube(ee);
dc.setLogger(node.getLogger());
let eeData;
if (collection['gee:type'] === 'image') {
eeData = ee.ImageCollection(ee.Image(id));
eeData = ee.Image(id);
}
else {
eeData = ee.ImageCollection(id);
Expand All @@ -32,10 +33,10 @@ export default class load_collection extends BaseProcess {
const spatial_extent = node.getArgument("spatial_extent");
if (spatial_extent !== null) {
if (spatial_extent.type) { // GeoJSON - has been validated before so `type` should be a safe indicator for GeoJSON
dc = Commons.filterGeoJSON(dc, spatial_extent, this.id, 'spatial_extent');
dc = Commons.filterGeoJSON(node, dc, spatial_extent, this.id, 'spatial_extent');
}
else { // Bounding box
dc = Commons.filterBbox(dc, spatial_extent, this.id, 'spatial_extent');
dc = Commons.filterBbox(node, dc, spatial_extent, this.id, 'spatial_extent');
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/processes/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class log extends BaseProcess {
case 10:
return image.log10();
default:
return image.log().divide(ee.Image(base).log());
return image.log().divide(node.ee.Image(base).log());
}
},
x => {
Expand Down
Loading

0 comments on commit 8d435fb

Please sign in to comment.