diff --git a/docker-compose.yaml b/docker-compose.yaml index d465a704..f015a71a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: diff --git a/example.env b/example.env index 2fa92ae3..a4ee4143 100644 --- a/example.env +++ b/example.env @@ -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= diff --git a/firebase/README.md b/firebase/README.md index fed47268..c3381abc 100644 --- a/firebase/README.md +++ b/firebase/README.md @@ -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). @@ -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. diff --git a/firebase/firebase.json b/firebase/firebase.json index 4c56a304..b81c0221 100644 --- a/firebase/firebase.json +++ b/firebase/firebase.json @@ -20,6 +20,14 @@ { "source": "/token", "function": "osmAuth-token" + }, + { + "source": "/redirectweb", + "function": "osmAuth-redirectweb" + }, + { + "source": "/tokenweb", + "function": "osmAuth-tokenweb" } ] }, diff --git a/firebase/functions/src/index.ts b/firebase/functions/src/index.ts index 793ed9c7..c448cadc 100644 --- a/firebase/functions/src/index.ts +++ b/firebase/functions/src/index.ts @@ -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 = {}; @@ -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. diff --git a/firebase/functions/src/osm_auth.ts b/firebase/functions/src/osm_auth.ts index d187b4e4..9953f2ea 100644 --- a/firebase/functions/src/osm_auth.ts +++ b/firebase/functions/src/osm_auth.ts @@ -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 @@ -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. @@ -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, @@ -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 = @@ -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) { @@ -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 () => { @@ -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, }); @@ -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); }); @@ -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); }; /** @@ -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. @@ -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;