Skip to content

Commit

Permalink
Implement Authentication via Google (#84)
Browse files Browse the repository at this point in the history
* Authentication with Google Account
* Request user information from Google
  • Loading branch information
m-mohr authored Jan 19, 2024
1 parent bb7f1f9 commit 9a58a77
Show file tree
Hide file tree
Showing 30 changed files with 355 additions and 150 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
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,40 @@ 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 "Web Application" 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/"
]
}
]
```

### 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": []
}
7 changes: 6 additions & 1 deletion src/adduser.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ if (password && password.length < 4) {
stop(1);
}

let email = await rl.question('Enter an email address (optional): ');
if (!email || email.length < 6) {
email = null;
}

try {
await users.register(username, password);
await users.register(username, password, email);
console.log('User created!');
stop(0);
} catch (err) {
Expand Down
37 changes: 30 additions & 7 deletions src/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ 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.serviceAccountCredentialsFile) {
server.addEndpoint('get', '/credentials/basic', this.getCredentialsBasic.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,16 +32,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: this.storage.oidcIssuer,
title: "Google",
description: "Login with your Google Earth Engine account.",
scopes: this.storage.oidcScopes,
default_clients: this.context.googleAuthClients
}
]
});
}

async getCredentialsBasic(req, res) {
if (!req.authorization.scheme) {
if (!this.context.serviceAccountCredentialsFile) {
throw new Errors.FeatureUnsupported();
}
else if (!req.authorization.scheme) {
throw new Errors.AuthenticationRequired();
}
else if (req.authorization.scheme !== 'Basic') {
Expand All @@ -59,6 +81,7 @@ export default class UsersAPI {
const data = {
user_id: req.user._id,
name: req.user.name,
email: req.user.email || null,
budget: null,
links: [
{
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: this.serverContext.serviceAccountCredentialsFile || null
});
const bucket = storage.bucket('earthengine-stac');
const prefix = 'catalog/';
Expand Down
94 changes: 89 additions & 5 deletions src/models/userstore.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ import Utils from '../utils/utils.js';
import DB from '../utils/db.js';
import Errors from '../utils/errors.js';
import crypto from "crypto";
import HttpUtils from '../utils/http.js';

export default class UserStore {

constructor() {
constructor(context) {
this.serverContext = context;

this.db = DB.load('users');

this.tokenDb = DB.load('token');
this.tokenValidity = 24*60*60;

this.oidcUserInfoEndpoint = null;
this.oidcIssuer = 'https://accounts.google.com';
this.oidcScopes = [
"openid",
"email",
"https://www.googleapis.com/auth/earthengine",
// "https://www.googleapis.com/auth/cloud-platform",
// "https://www.googleapis.com/auth/devstorage.full_control"
];
}

database() {
Expand All @@ -35,6 +49,7 @@ export default class UserStore {
emptyUser(withId = true) {
const user = {
name: null,
email: null,
password: null,
passwordSalt: null
};
Expand Down Expand Up @@ -92,18 +107,19 @@ export default class UserStore {
return user !== null;
}

async register(name, password) {
async register(name, password, email = null) {
const userData = this.emptyUser(false);
const pw = this.encryptPassword(password);
userData.name = name;
userData.email = email;
userData.password = pw.passwordHash;
userData.passwordSalt = pw.salt;
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 +136,76 @@ export default class UserStore {
reason: 'User account has been removed.'
});
}

return user;
}

async authenticateGoogle(token) {
const userInfo = await this.getOidcUserInfo(token);
const userData = this.emptyUser(false);
userData._id = "google_" + userInfo.sub;
userData.name = userInfo.name || userInfo.email || null;
userData.email = userInfo.email || null;
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;
// todo: database handling for less OIDC userInfo requests
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.'
});
}
}

async getOidcUserInfoEndpoint() {
if (this.oidcUserInfoEndpoint === null) {
try {
const url = this.oidcIssuer + '/.well-known/openid-configuration';
const doc = await HttpUtils.get(url);
this.oidcUserInfoEndpoint = doc.userinfo_endpoint || null;
} catch (err) {
throw new Errors.Internal({
message: 'Can not retrieve OIDC well-known document: ' + err.message
});
}
}
return this.oidcUserInfoEndpoint;
}

async getOidcUserInfo(token) {
const endpoint = await this.getOidcUserInfoEndpoint();
if (endpoint) {
return HttpUtils.get(endpoint, {Authorization: `Bearer ${token}`});
}
else {
throw new Errors.Internal({
message: 'Can not retrieve user information from Google.'
});
}
}

}
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
Loading

0 comments on commit 9a58a77

Please sign in to comment.