Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
10 changes: 9 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,23 @@ services:
OSM_OAUTH_API_URL: '${OSM_OAUTH_API_URL}'
OSM_OAUTH_CLIENT_ID: '${OSM_OAUTH_CLIENT_ID}'
OSM_OAUTH_CLIENT_SECRET: '${OSM_OAUTH_CLIENT_SECRET}'
OSM_OAUTH_REDIRECT_URI_WEB: '${OSM_OAUTH_REDIRECT_URI_WEB}'
OSM_OAUTH_APP_LOGIN_LINK_WEB: '${OSM_OAUTH_APP_LOGIN_LINK_WEB}'
OSM_OAUTH_CLIENT_ID_WEB: '${OSM_OAUTH_CLIENT_ID_WEB}'
OSM_OAUTH_CLIENT_SECRET_WEB: '${OSM_OAUTH_CLIENT_SECRET_WEB}'
command: >-
sh -c "firebase use $FIREBASE_DB &&
firebase target:apply hosting auth \"$FIREBASE_AUTH_SITE\" &&
firebase functions:config:set
osm.redirect_uri=\"$OSM_OAUTH_REDIRECT_URI\"
osm.redirect_uri_web=\"$OSM_OAUTH_REDIRECT_URI_WEB\"
osm.app_login_link=\"$OSM_OAUTH_APP_LOGIN_LINK\"
osm.app_login_link_web=\"$OSM_OAUTH_APP_LOGIN_LINK_WEB\"
osm.api_url=\"$OSM_OAUTH_API_URL\"
osm.client_id=\"$OSM_OAUTH_CLIENT_ID\"
osm.client_secret=\"$OSM_OAUTH_CLIENT_SECRET\" &&
osm.client_id_web=\"$OSM_OAUTH_CLIENT_ID_WEB\"
osm.client_secret=\"$OSM_OAUTH_CLIENT_SECRET\"
osm.client_secret_web=\"$OSM_OAUTH_CLIENT_SECRET_WEB\" &&
firebase deploy --token $FIREBASE_TOKEN --only functions,hosting,database"

django:
Expand Down
7 changes: 6 additions & 1 deletion example.env
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ OSMCHA_API_KEY=

# OSM OAuth Configuration
OSM_OAUTH_REDIRECT_URI=
OSM_OAUTH_REDIRECT_URI_WEB=
OSM_OAUTH_API_URL=
OSM_OAUTH_CLIENT_ID=
OSM_OAUTH_CLIENT_ID_WEB=
OSM_OAUTH_CLIENT_SECRET=
OSM_APP_LOGIN_LINK=
OSM_OAUTH_CLIENT_SECRET_WEB=
OSM_OAUTH_APP_LOGIN_LINK=
OSM_OAUTH_APP_LOGIN_LINK_WEB=


# DJANGO For more info look at django/mapswipe/settings.py::L22
DJANGO_SECRET_KEY=
Expand Down
9 changes: 9 additions & 0 deletions firebase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ expose the authentication functions publicly.
* `firebase deploy --only functions,hosting`
* `firebase deploy --only database:rules`

## Deploy with Makefile
You can also deploy the changes to Firebase using make:
* Make sure to remove the firebase_deploy docker image first: `docker rmi python-mapswipe-workers-firebase_deploy`
* `make update_firebase_functions_and_db_rules`

## Notes on OAuth (OSM login)

Refer to [the notes in the app repository](https://github.com/mapswipe/mapswipe/blob/master/docs/osm_login.md).
Expand All @@ -30,12 +35,16 @@ Some specifics about the related functions:
- Before deploying, set the required firebase config values in environment:
FIXME: replace env vars with config value names
- OSM_OAUTH_REDIRECT_URI `osm.redirect_uri`: `https://dev-auth.mapswipe.org/token` or `https://auth.mapswipe.org/token`
- OSM_OAUTH_REDIRECT_URI_WEB: `https://dev-auth.mapswipe.org/tokenweb` or `https://auth.mapswipe.org/tokenweb`
- OSM_OAUTH_APP_LOGIN_LINK `osm.app_login_link`: 'devmapswipe://login/osm' or 'mapswipe://login/osm'
- OSM_OAUTH_APP_LOGIN_LINK_WEB: `https://web.mapswipe.org/dev/#/osm-callback` or `https://web.mapswipe.org/#/osm-callback`
- OSM_OAUTH_API_URL `osm.api_url`: 'https://master.apis.dev.openstreetmap.org/' or 'https://www.openstreetmap.org/' (include the
trailing slash)
- OSM_OAUTH_CLIENT_ID `osm.client_id`: find it on the OSM application page
- OSM_OAUTH_CLIENT_SECRET `osm.client_secret`: same as above. Note that this can only be seen once when the application is created. Do not
lose it!
- OSM_OAUTH_CLIENT_ID_WEB: This is the ID of a __different__ registered OSM OAuth client for the web version that needs to have `https://dev-auth.mapswipe.org/tokenweb` or `https://auth.mapswipe.org/tokenweb` set as redirect URI.
- OSM_OAUTH_CLIENT_SECRET_WEB: This is the secret of the OSM OAuth client for MapSwipe web version.
- Deploy the functions as explained above
- Expose the functions publicly through firebase hosting, this is done in `/firebase/firebase.json` under the `hosting`
key.
Expand Down
8 changes: 8 additions & 0 deletions firebase/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@
{
"source": "/token",
"function": "osmAuth-token"
},
{
"source": "/redirectweb",
"function": "osmAuth-redirectweb"
},
{
"source": "/tokenweb",
"function": "osmAuth-tokenweb"
}
]
},
Expand Down
10 changes: 9 additions & 1 deletion firebase/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ admin.initializeApp();
// all functions are bundled together. It's less than ideal, but it does not
// seem possible to split them using the split system for multiple sites from
// https://firebase.google.com/docs/hosting/multisites
import {redirect, token} from './osm_auth';
import {redirect, token, redirectweb, tokenweb} from './osm_auth';
import { formatProjectTopic, formatUserName } from './utils';

exports.osmAuth = {};
Expand All @@ -23,6 +23,14 @@ exports.osmAuth.token = functions.https.onRequest((req, res) => {
token(req, res, admin);
});

exports.osmAuth.redirectweb = functions.https.onRequest((req, res) => {
redirectweb(req, res);
});

exports.osmAuth.tokenweb = functions.https.onRequest((req, res) => {
tokenweb(req, res, admin);
});

/*
Log the userIds of all users who finished a group to /v2/userGroups/{projectId}/{groupId}/.
Gets triggered when new results of a group are written to the database.
Expand Down
94 changes: 70 additions & 24 deletions firebase/functions/src/osm_auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Firebase cloud functions to allow authentication with OpenStreet Map
// Firebase cloud functions to allow authentication with OpenStreetMap
//
// There are really 2 functions, which must be publicly accessible via
// an https endpoint. They can be hosted on firebase under a domain like
Expand All @@ -20,8 +20,10 @@ import axios from 'axios';
// will get a cryptic error about the server not being able to continue
// TODO: adjust the prefix based on which deployment is done (prod/dev)
const OAUTH_REDIRECT_URI = functions.config().osm?.redirect_uri;
const OAUTH_REDIRECT_URI_WEB = functions.config().osm?.redirect_uri_web;

const APP_OSM_LOGIN_DEEPLINK = functions.config().osm?.app_login_link;
const APP_OSM_LOGIN_DEEPLINK_WEB = functions.config().osm?.app_login_link_web;

// the scope is taken from https://wiki.openstreetmap.org/wiki/OAuth#OAuth_2.0
// at least one seems to be required for the auth workflow to complete.
Expand All @@ -36,11 +38,11 @@ const OSM_API_URL = functions.config().osm?.api_url;
* Configure the `osm.client_id` and `osm.client_secret`
* Google Cloud environment variables for the values below to exist
*/
function osmOAuth2Client() {
function osmOAuth2Client(client_id: any, client_secret: any) {
const credentials = {
client: {
id: functions.config().osm?.client_id,
secret: functions.config().osm?.client_secret,
id: client_id,
secret: client_secret,
},
auth: {
tokenHost: OSM_API_URL,
Expand All @@ -58,8 +60,8 @@ function osmOAuth2Client() {
* NOT a webview inside MapSwipe, as this would break the promise of
* OAuth that we do not touch their OSM credentials
*/
export const redirect = (req: any, res: any) => {
const oauth2 = osmOAuth2Client();
function redirect2OsmOauth(req: any, res: any, redirect_uri: string, client_id: string, client_secret: string) {
const oauth2 = osmOAuth2Client(client_id, client_secret);

cookieParser()(req, res, () => {
const state =
Expand All @@ -75,17 +77,31 @@ export const redirect = (req: any, res: any) => {
httpOnly: true,
});
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
redirect_uri: redirect_uri,
scope: OAUTH_SCOPES,
state: state,
});
functions.logger.log('Redirecting to:', redirectUri);
res.redirect(redirectUri);
});
}

export const redirect = (req: any, res: any) => {
const redirect_uri = OAUTH_REDIRECT_URI;
const client_id = functions.config().osm?.client_id;
const client_secret = functions.config().osm?.client_secret;
redirect2OsmOauth(req, res, redirect_uri, client_id, client_secret);
};

export const redirectweb = (req: any, res: any) => {
const redirect_uri = OAUTH_REDIRECT_URI_WEB;
const client_id = functions.config().osm?.client_id_web;
const client_secret = functions.config().osm?.client_secret_web;
redirect2OsmOauth(req, res, redirect_uri, client_id, client_secret);
};

/**
* The OSM OAuth endpoing does not give us any info about the user,
* The OSM OAuth endpoint does not give us any info about the user,
* so we need to get the user profile from this endpoint
*/
async function getOSMProfile(accessToken: string) {
Expand All @@ -107,8 +123,8 @@ async function getOSMProfile(accessToken: string) {
* The Firebase custom auth token, display name, photo URL and OSM access
* token are sent back to the app via a deeplink redirect.
*/
export const token = async (req: any, res: any, admin: any) => {
const oauth2 = osmOAuth2Client();
function fbToken(req: any, res: any, admin: any, redirect_uri: string, osm_login_link: string, client_id: string, client_web: string) {
const oauth2 = osmOAuth2Client(client_id, client_web);

try {
return cookieParser()(req, res, async () => {
Expand Down Expand Up @@ -139,7 +155,7 @@ export const token = async (req: any, res: any, admin: any) => {
// this doesn't work
results = await oauth2.authorizationCode.getToken({
code: req.query.code,
redirect_uri: OAUTH_REDIRECT_URI,
redirect_uri: redirect_uri,
scope: OAUTH_SCOPES,
state: req.query.state,
});
Expand Down Expand Up @@ -177,7 +193,7 @@ export const token = async (req: any, res: any, admin: any) => {
);
// build a deep link so we can send the token back to the app
// from the browser
const signinUrl = `${APP_OSM_LOGIN_DEEPLINK}?token=${firebaseToken}`;
const signinUrl = `${osm_login_link}?token=${firebaseToken}`;
functions.logger.log('redirecting user to', signinUrl);
res.redirect(signinUrl);
});
Expand All @@ -187,6 +203,22 @@ export const token = async (req: any, res: any, admin: any) => {
// back into the app to allow the user to take action
return res.json({ error: error.toString() });
}
}

export const token = async (req: any, res: any, admin: any) => {
const redirect_uri = OAUTH_REDIRECT_URI;
const osm_login_link = APP_OSM_LOGIN_DEEPLINK;
const client_id = functions.config().osm?.client_id;
const client_secret = functions.config().osm?.client_secret;
fbToken(req, res, admin, redirect_uri, osm_login_link, client_id, client_secret);
};

export const tokenweb = async (req: any, res: any, admin: any) => {
const redirect_uri = OAUTH_REDIRECT_URI_WEB;
const osm_login_link = APP_OSM_LOGIN_DEEPLINK_WEB;
const client_id = functions.config().osm?.client_id_web;
const client_secret = functions.config().osm?.client_secret_web;
fbToken(req, res, admin, redirect_uri, osm_login_link, client_id, client_secret);
};

/**
Expand All @@ -204,23 +236,18 @@ async function createFirebaseAccount(admin: any, osmID: any, displayName: any, a
// with a variable length.
const uid = `osm:${osmID}`;

const profileRef = admin.database().ref(`v2/users/${uid}`);

// check if profile exists on Firebase Realtime Database
const snapshot = await profileRef.once('value');
const profileExists = snapshot.exists();

// Save the access token to the Firebase Realtime Database.
const databaseTask = admin
.database()
.ref(`v2/OSMAccessToken/${uid}`)
.set(accessToken);

const profileTask = admin
.database()
.ref(`v2/users/${uid}/`)
.set({
created: new Date().toISOString(),
groupContributionCount: 0,
projectContributionCount: 0,
taskContributionCount: 0,
displayName,
});

// Create or update the firebase user account.
// This does not login the user on the app, it just ensures that a firebase
// user account (linked to the OSM account) exists.
Expand All @@ -240,8 +267,27 @@ async function createFirebaseAccount(admin: any, osmID: any, displayName: any, a
throw error;
});

// If profile exists, only update displayName -- else create new user profile
const tasks = [userCreationTask, databaseTask];
if (profileExists) {
functions.logger.log('Sign in to existing OSM profile');
const profileUpdateTask = profileRef.update({ displayName: displayName });
tasks.push(profileUpdateTask);
} else {
functions.logger.log('Sign up new OSM profile');
const profileCreationTask = profileRef
.set({
created: new Date().toISOString(),
groupContributionCount: 0,
projectContributionCount: 0,
taskContributionCount: 0,
displayName,
});
tasks.push(profileCreationTask);
}

// Wait for all async task to complete then generate and return a custom auth token.
await Promise.all([userCreationTask, databaseTask, profileTask]);
await Promise.all(tasks);
// Create a Firebase custom auth token.
functions.logger.log('In createFirebaseAccount: createCustomToken');
let authToken;
Expand Down