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

Polishing some aspects of the application #11

Merged
merged 11 commits into from
Jun 7, 2022
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ npm start
## Built with

- [Carbon Design System](https://github.com/Philipsty/carbon-angular-starter) - web framework used
- [IBM Cloudant](https://cloud.ibm.com/catalog?search=cloudant#search_results) - The NoSQL database used
- MongoDB - NoSQL database
- Node.js
- IBM Cloud

## Authors
Expand Down
1 change: 1 addition & 0 deletions backend/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
7 changes: 7 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "OpenHarvest-backend",
"version": "1.0.0",
"description": "",
"engines": {
"node": ">=16"
},
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
Expand All @@ -13,6 +16,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@turf/turf": "^6.5.0",
"@types/geojson": "^7946.0.8",
"@types/uuid": "^8.3.4",
"axios": "^0.26.0",
Expand All @@ -22,9 +26,11 @@
"express": "^4.17.2",
"express-session": "^1.17.2",
"geojson": "^0.5.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.2.1",
"passport": "^0.5.2",
"passport-ci-oidc": "^2.0.5",
"passport-jwt": "^4.0.0",
"socket.io": "^4.4.1",
"twilio": "^3.75.1",
"uuid": "^8.3.2"
Expand All @@ -33,6 +39,7 @@
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/jsonwebtoken": "^8.5.8",
"nodemon": "^2.0.15",
"ts-node": "^10.5.0",
"typescript": "^4.5.5"
Expand Down
37 changes: 37 additions & 0 deletions backend/src/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Authentication
Authentication in OpenHarvest is a bit complicated with many places where verification is completed and data is stored and returned.

## User Authentication Steps
1. User navigates to https://openhavest.net/auth/login
2. API realises someone is trying to connect, redirect to IBMid
1. We use `passport` & `passport-cu-oidc` strategy for auth middleware
2. `passport-cu-oidc` uses our config to redirect automatically
3. User logs into IBMid via OAuth, OpenID
4. IBMid service calls back to `https://openhavest.net/auth/sso/callback` with the token
5. `passport-cu-oidc` is attached to this url, it parses and handles the data from IBMid
6. `passport-cu-oidc` executes our [function](https://github.com/Call-for-Code/OpenHarvest/blob/8c957bdffc570b786fbc7e971ac44c841d76c311/backend/src/auth/IBMiDStrategy.ts#L14) to populate their data and store it on the `req.user` object.
7. Our handler for `/auth/login` now executes and we can read `req.user` and create a new JWT using it.
8. We redirect back to the web application with the token as a http param: `https://openhavest.net/?token=...`
9. At this stage auth details are stored in the session based user storage and in the JWT token

This Sequence diagram attempts to explain it.

```mermaid
sequenceDiagram
participant client as Client (Web App)
participant api as OpenHarvest API
participant ibmid as IBMId
client->>api: /auth/login
api-->>client: 302 Redirect to IBMId for Auth
client->>ibmid: Authenticates using
ibmid->>api: "/auth/sso/callback": With user details
api->client: Updates session with user details
api->client: Generates a JWT Token for user
api->>client: "/?token=..." attaches token to a http param
opt Get User Details
client->>api: Client asks for users details
end
opt API request
client->>api: Call OpenHarvest API
end
```
69 changes: 69 additions & 0 deletions backend/src/auth/auth-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Router } from "express";
import passport from "passport";
import { formatUser, ensureAuthenticated } from "./helpers";
import { IBMidStrategy } from "./IBMiDStrategy";
import { JWTOpenHarvestStrategy, opts } from "./jwtStrategy";
import jwt from 'jsonwebtoken';

passport.serializeUser(function(user, done) {
done(null, user);
});

passport.deserializeUser(function(obj, done) {
done(null, obj);
});

const redirect_url = process.env.NODE_ENV == "production" ? "https://openharvest.net/" : "http://localhost:3001/";

passport.use(IBMidStrategy);
passport.use(JWTOpenHarvestStrategy);

const router = Router();

router.get('/login', passport.authenticate('openidconnect', { state: Math.random().toString(36).substr(2, 10) }));

/**
* This handles the callback from IBMid
* Complete url: '/auth/sso/callback'
*/
router.get(
'/sso/callback',
passport.authenticate('openidconnect', {failureRedirect: '/failure'}),
function (req, res) {
// Encode a new JWT token and pass it to the web app via a http param
const formattedUser = formatUser(req.user);
const token = jwt.sign(formattedUser!!, opts.secretOrKey!!);
return res.redirect(redirect_url + "?token=" + token);
}
);

// failure page
router.get('/failure', function(req, res) {
res.send('login failed');
});


router.get('/my_details_openid', ensureAuthenticated, function (req, res) {
var claims = req.user['_json'];
var html ="<p>Hello " + claims.given_name + " " + claims.family_name + ": </p>";

html += "User details (ID token in _json object): </p>";

html += "<pre>" + JSON.stringify(req.user, null, 4) + "</pre>";

html += "<br /><a href=\"/logout\">logout</a>";

html += "<hr> <a href=\"/\">home</a>";

//res.send('Hello '+ claims.given_name + ' ' + claims.family_name + ', your email is ' + claims.email + '<br /> <a href=\'/\'>home</a>');

res.send(html);
});



router.get('/me', ensureAuthenticated, (req, res) => {
return res.json(formatUser(req.user));
});

export const AuthRoutes = router;
14 changes: 14 additions & 0 deletions backend/src/auth/jwtStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";

export const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.jwt_secret,
issuer: 'http://openharvest.net',
audience: 'http://openharvest.net'
}

export const JWTOpenHarvestStrategy = new JwtStrategy(opts, (jwt_payload, done) => {
return jwt_payload;
});


5 changes: 2 additions & 3 deletions backend/src/db/entities/farmer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

import { FieldResponse } from './../../integrations/EIS/EIS.types';
import { Schema, model, ObjectId, Types } from 'mongoose';
import { Land } from './land';
import { Field } from "./field";

const ObjectId = Schema.Types.ObjectId;

Expand All @@ -12,7 +11,7 @@ export interface Farmer {
address: string,
coopOrganisations: string[],
fieldCount: number;
field?: FieldResponse;
field?: Field;
}

export const FarmerSchema = new Schema({
Expand Down
73 changes: 73 additions & 0 deletions backend/src/db/entities/field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Feature, Polygon } from "geojson";
import { Schema, model, ObjectId, Types } from 'mongoose';
import { Crop } from "./crop";
const ObjectId = Schema.Types.ObjectId;

export interface SubFieldCrop {
planted: Date;
harvested: Date | null;
farmer: string;
/**
* Crop Information
*/
crop: Crop;
}

export interface SubFieldProperties {
/**
* Area in Square Meters
*/
area: number;
bbox: {
northEast: {lat: number, lng: number},
southWest: {lat: number, lng: number}
},
centre: {
lat: number,
lng: number
}
crops: SubFieldCrop[]
}

export interface SubField extends Feature<Polygon, SubFieldProperties> {
_id?: Types.ObjectId;
name: string;
}

export const SubFieldSchema = new Schema({
_id: {
type: ObjectId,
auto: true
},
name: String,
type: String,
properties: Object,
geometry: Object
});

export interface Field {
_id?: Types.ObjectId;
farmer_id: string,
bbox: {
northEast: {lat: number, lng: number},
southWest: {lat: number, lng: number}
},
centre?: {
lat: number,
lng: number
}
subFields: SubField[];
}

export const FieldSchema = new Schema({
_id: {
type: ObjectId,
auto: true
},
// coopOrganisations: [String], // The field belongs to the org of the farmer
farmer_id: String,
bbox: Object,
subFields: [SubFieldSchema]
});

export const FieldModel = model<Field>("field", FieldSchema);
71 changes: 7 additions & 64 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import path from "path";
import fs from 'fs'
import express from "express";
import session from "express-session";
import https from 'https'
import passport from "passport";
import https from 'https';
import cookieParser from "cookie-parser";
import cors from "cors";
import bodyParser from "body-parser";
Expand All @@ -23,10 +22,9 @@ import messageLogRoutes from "./routes/messaging-route";
import smsRoutes from "./routes/sms-route";
import foodTrustRoutes from "./routes/food-trust-route";

import { formatUser, ensureAuthenticated } from "./auth/helpers";
import { IBMidStrategy } from "./auth/IBMiDStrategy";
import { SocketIOManager, SocketIOManagerInstance } from "./sockets/socket.io";
import { Server } from "http";
import { AuthRoutes } from "./auth/auth-route";

mongoInit();

Expand All @@ -38,79 +36,24 @@ app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// Enable session
// Enable session for the sole reason of passport-ci-oidc. WE ARE NOT SUPPORTING SESSION BASED AUTH
app.use(cookieParser());
app.use(session({
secret: "test",
secret: process.env.jwt_secret!!,
resave: true,
saveUninitialized: true
}));

// Passport
app.use(passport.initialize());
app.use(passport.session());
// app.use(passport.initialize()); // This is only needed if we're using sessions: https://stackoverflow.com/a/56095662
// app.use(passport.session());

passport.serializeUser(function(user, done) {
done(null, user);
});

passport.deserializeUser(function(obj, done) {
done(null, obj);
});

passport.use(IBMidStrategy);

app.get('/login', passport.authenticate('openidconnect', { state: Math.random().toString(36).substr(2, 10) }));

app.get('/auth/sso/callback', function (req, res, next) {
// @ts-ignore
let redirect_url = "/app";
if (process.env.NODE_ENV == "production") {
redirect_url = req.session.originalUrl;
redirect_url = "https://openharvest.net/";
}
else {
redirect_url = "http://localhost:3001/";
}
passport.authenticate('openidconnect', {
successRedirect: redirect_url,
// successRedirect: '/hello',
failureRedirect: '/failure'
})(req, res, next);
});

// failure page
app.get('/failure', function(req, res) {
res.send('login failed');
});


app.get('/hello', ensureAuthenticated, function (req, res) {
var claims = req.user['_json'];
var html ="<p>Hello " + claims.given_name + " " + claims.family_name + ": </p>";

html += "User details (ID token in _json object): </p>";

html += "<pre>" + JSON.stringify(req.user, null, 4) + "</pre>";

html += "<br /><a href=\"/logout\">logout</a>";

html += "<hr> <a href=\"/\">home</a>";

//res.send('Hello '+ claims.given_name + ' ' + claims.family_name + ', your email is ' + claims.email + '<br /> <a href=\'/\'>home</a>');

res.send(html);
});



app.get('/me', ensureAuthenticated, (req, res) => {
return res.json(formatUser(req.user));
});

// routes and api calls
// app.use('/api', healthRoutes);
// app.use('/api/names', nameRoutes);
app.use("/auth/", AuthRoutes);

app.use("/api/farmer", farmerRoutes);
app.use("/api/lot", lotRoutes);
Expand Down
Loading