Skip to content

Commit 9380c13

Browse files
authored
Merge pull request #1031 from mapswipe/osm-login-web
Provide redirect and token Firebase functions to allow OSM log in for MapSwipe web
2 parents 408fb60 + 8b4941f commit 9380c13

File tree

6 files changed

+111
-27
lines changed

6 files changed

+111
-27
lines changed

docker-compose.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,23 @@ services:
230230
OSM_OAUTH_API_URL: '${OSM_OAUTH_API_URL}'
231231
OSM_OAUTH_CLIENT_ID: '${OSM_OAUTH_CLIENT_ID}'
232232
OSM_OAUTH_CLIENT_SECRET: '${OSM_OAUTH_CLIENT_SECRET}'
233+
OSM_OAUTH_REDIRECT_URI_WEB: '${OSM_OAUTH_REDIRECT_URI_WEB}'
234+
OSM_OAUTH_APP_LOGIN_LINK_WEB: '${OSM_OAUTH_APP_LOGIN_LINK_WEB}'
235+
OSM_OAUTH_CLIENT_ID_WEB: '${OSM_OAUTH_CLIENT_ID_WEB}'
236+
OSM_OAUTH_CLIENT_SECRET_WEB: '${OSM_OAUTH_CLIENT_SECRET_WEB}'
233237
command: >-
234238
sh -c "firebase use $FIREBASE_DB &&
235239
firebase target:apply hosting auth \"$FIREBASE_AUTH_SITE\" &&
236240
firebase functions:config:set
237241
osm.redirect_uri=\"$OSM_OAUTH_REDIRECT_URI\"
242+
osm.redirect_uri_web=\"$OSM_OAUTH_REDIRECT_URI_WEB\"
238243
osm.app_login_link=\"$OSM_OAUTH_APP_LOGIN_LINK\"
244+
osm.app_login_link_web=\"$OSM_OAUTH_APP_LOGIN_LINK_WEB\"
239245
osm.api_url=\"$OSM_OAUTH_API_URL\"
240246
osm.client_id=\"$OSM_OAUTH_CLIENT_ID\"
241-
osm.client_secret=\"$OSM_OAUTH_CLIENT_SECRET\" &&
247+
osm.client_id_web=\"$OSM_OAUTH_CLIENT_ID_WEB\"
248+
osm.client_secret=\"$OSM_OAUTH_CLIENT_SECRET\"
249+
osm.client_secret_web=\"$OSM_OAUTH_CLIENT_SECRET_WEB\" &&
242250
firebase deploy --token $FIREBASE_TOKEN --only functions,hosting,database"
243251
244252
django:

example.env

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,15 @@ OSMCHA_API_KEY=
3838

3939
# OSM OAuth Configuration
4040
OSM_OAUTH_REDIRECT_URI=
41+
OSM_OAUTH_REDIRECT_URI_WEB=
4142
OSM_OAUTH_API_URL=
4243
OSM_OAUTH_CLIENT_ID=
44+
OSM_OAUTH_CLIENT_ID_WEB=
4345
OSM_OAUTH_CLIENT_SECRET=
44-
OSM_APP_LOGIN_LINK=
46+
OSM_OAUTH_CLIENT_SECRET_WEB=
47+
OSM_OAUTH_APP_LOGIN_LINK=
48+
OSM_OAUTH_APP_LOGIN_LINK_WEB=
49+
4550

4651
# DJANGO For more info look at django/mapswipe/settings.py::L22
4752
DJANGO_SECRET_KEY=

firebase/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ expose the authentication functions publicly.
2020
* `firebase deploy --only functions,hosting`
2121
* `firebase deploy --only database:rules`
2222

23+
## Deploy with Makefile
24+
You can also deploy the changes to Firebase using make:
25+
* Make sure to remove the firebase_deploy docker image first: `docker rmi python-mapswipe-workers-firebase_deploy`
26+
* `make update_firebase_functions_and_db_rules`
27+
2328
## Notes on OAuth (OSM login)
2429

2530
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:
3035
- Before deploying, set the required firebase config values in environment:
3136
FIXME: replace env vars with config value names
3237
- OSM_OAUTH_REDIRECT_URI `osm.redirect_uri`: `https://dev-auth.mapswipe.org/token` or `https://auth.mapswipe.org/token`
38+
- OSM_OAUTH_REDIRECT_URI_WEB: `https://dev-auth.mapswipe.org/tokenweb` or `https://auth.mapswipe.org/tokenweb`
3339
- OSM_OAUTH_APP_LOGIN_LINK `osm.app_login_link`: 'devmapswipe://login/osm' or 'mapswipe://login/osm'
40+
- OSM_OAUTH_APP_LOGIN_LINK_WEB: `https://web.mapswipe.org/dev/#/osm-callback` or `https://web.mapswipe.org/#/osm-callback`
3441
- OSM_OAUTH_API_URL `osm.api_url`: 'https://master.apis.dev.openstreetmap.org/' or 'https://www.openstreetmap.org/' (include the
3542
trailing slash)
3643
- OSM_OAUTH_CLIENT_ID `osm.client_id`: find it on the OSM application page
3744
- 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
3845
lose it!
46+
- 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.
47+
- OSM_OAUTH_CLIENT_SECRET_WEB: This is the secret of the OSM OAuth client for MapSwipe web version.
3948
- Deploy the functions as explained above
4049
- Expose the functions publicly through firebase hosting, this is done in `/firebase/firebase.json` under the `hosting`
4150
key.

firebase/firebase.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
{
2121
"source": "/token",
2222
"function": "osmAuth-token"
23+
},
24+
{
25+
"source": "/redirectweb",
26+
"function": "osmAuth-redirectweb"
27+
},
28+
{
29+
"source": "/tokenweb",
30+
"function": "osmAuth-tokenweb"
2331
}
2432
]
2533
},

firebase/functions/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ admin.initializeApp();
88
// all functions are bundled together. It's less than ideal, but it does not
99
// seem possible to split them using the split system for multiple sites from
1010
// https://firebase.google.com/docs/hosting/multisites
11-
import {redirect, token} from './osm_auth';
11+
import {redirect, token, redirectweb, tokenweb} from './osm_auth';
1212
import { formatProjectTopic, formatUserName } from './utils';
1313

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

26+
exports.osmAuth.redirectweb = functions.https.onRequest((req, res) => {
27+
redirectweb(req, res);
28+
});
29+
30+
exports.osmAuth.tokenweb = functions.https.onRequest((req, res) => {
31+
tokenweb(req, res, admin);
32+
});
33+
2634
/*
2735
Log the userIds of all users who finished a group to /v2/userGroups/{projectId}/{groupId}/.
2836
Gets triggered when new results of a group are written to the database.

firebase/functions/src/osm_auth.ts

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Firebase cloud functions to allow authentication with OpenStreet Map
1+
// Firebase cloud functions to allow authentication with OpenStreetMap
22
//
33
// There are really 2 functions, which must be publicly accessible via
44
// an https endpoint. They can be hosted on firebase under a domain like
@@ -20,8 +20,10 @@ import axios from 'axios';
2020
// will get a cryptic error about the server not being able to continue
2121
// TODO: adjust the prefix based on which deployment is done (prod/dev)
2222
const OAUTH_REDIRECT_URI = functions.config().osm?.redirect_uri;
23+
const OAUTH_REDIRECT_URI_WEB = functions.config().osm?.redirect_uri_web;
2324

2425
const APP_OSM_LOGIN_DEEPLINK = functions.config().osm?.app_login_link;
26+
const APP_OSM_LOGIN_DEEPLINK_WEB = functions.config().osm?.app_login_link_web;
2527

2628
// the scope is taken from https://wiki.openstreetmap.org/wiki/OAuth#OAuth_2.0
2729
// 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;
3638
* Configure the `osm.client_id` and `osm.client_secret`
3739
* Google Cloud environment variables for the values below to exist
3840
*/
39-
function osmOAuth2Client() {
41+
function osmOAuth2Client(client_id: any, client_secret: any) {
4042
const credentials = {
4143
client: {
42-
id: functions.config().osm?.client_id,
43-
secret: functions.config().osm?.client_secret,
44+
id: client_id,
45+
secret: client_secret,
4446
},
4547
auth: {
4648
tokenHost: OSM_API_URL,
@@ -58,8 +60,8 @@ function osmOAuth2Client() {
5860
* NOT a webview inside MapSwipe, as this would break the promise of
5961
* OAuth that we do not touch their OSM credentials
6062
*/
61-
export const redirect = (req: any, res: any) => {
62-
const oauth2 = osmOAuth2Client();
63+
function redirect2OsmOauth(req: any, res: any, redirect_uri: string, client_id: string, client_secret: string) {
64+
const oauth2 = osmOAuth2Client(client_id, client_secret);
6365

6466
cookieParser()(req, res, () => {
6567
const state =
@@ -75,17 +77,31 @@ export const redirect = (req: any, res: any) => {
7577
httpOnly: true,
7678
});
7779
const redirectUri = oauth2.authorizationCode.authorizeURL({
78-
redirect_uri: OAUTH_REDIRECT_URI,
80+
redirect_uri: redirect_uri,
7981
scope: OAUTH_SCOPES,
8082
state: state,
8183
});
8284
functions.logger.log('Redirecting to:', redirectUri);
8385
res.redirect(redirectUri);
8486
});
87+
}
88+
89+
export const redirect = (req: any, res: any) => {
90+
const redirect_uri = OAUTH_REDIRECT_URI;
91+
const client_id = functions.config().osm?.client_id;
92+
const client_secret = functions.config().osm?.client_secret;
93+
redirect2OsmOauth(req, res, redirect_uri, client_id, client_secret);
94+
};
95+
96+
export const redirectweb = (req: any, res: any) => {
97+
const redirect_uri = OAUTH_REDIRECT_URI_WEB;
98+
const client_id = functions.config().osm?.client_id_web;
99+
const client_secret = functions.config().osm?.client_secret_web;
100+
redirect2OsmOauth(req, res, redirect_uri, client_id, client_secret);
85101
};
86102

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

113129
try {
114130
return cookieParser()(req, res, async () => {
@@ -139,7 +155,7 @@ export const token = async (req: any, res: any, admin: any) => {
139155
// this doesn't work
140156
results = await oauth2.authorizationCode.getToken({
141157
code: req.query.code,
142-
redirect_uri: OAUTH_REDIRECT_URI,
158+
redirect_uri: redirect_uri,
143159
scope: OAUTH_SCOPES,
144160
state: req.query.state,
145161
});
@@ -177,7 +193,7 @@ export const token = async (req: any, res: any, admin: any) => {
177193
);
178194
// build a deep link so we can send the token back to the app
179195
// from the browser
180-
const signinUrl = `${APP_OSM_LOGIN_DEEPLINK}?token=${firebaseToken}`;
196+
const signinUrl = `${osm_login_link}?token=${firebaseToken}`;
181197
functions.logger.log('redirecting user to', signinUrl);
182198
res.redirect(signinUrl);
183199
});
@@ -187,6 +203,22 @@ export const token = async (req: any, res: any, admin: any) => {
187203
// back into the app to allow the user to take action
188204
return res.json({ error: error.toString() });
189205
}
206+
}
207+
208+
export const token = async (req: any, res: any, admin: any) => {
209+
const redirect_uri = OAUTH_REDIRECT_URI;
210+
const osm_login_link = APP_OSM_LOGIN_DEEPLINK;
211+
const client_id = functions.config().osm?.client_id;
212+
const client_secret = functions.config().osm?.client_secret;
213+
fbToken(req, res, admin, redirect_uri, osm_login_link, client_id, client_secret);
214+
};
215+
216+
export const tokenweb = async (req: any, res: any, admin: any) => {
217+
const redirect_uri = OAUTH_REDIRECT_URI_WEB;
218+
const osm_login_link = APP_OSM_LOGIN_DEEPLINK_WEB;
219+
const client_id = functions.config().osm?.client_id_web;
220+
const client_secret = functions.config().osm?.client_secret_web;
221+
fbToken(req, res, admin, redirect_uri, osm_login_link, client_id, client_secret);
190222
};
191223

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

239+
const profileRef = admin.database().ref(`v2/users/${uid}`);
240+
241+
// check if profile exists on Firebase Realtime Database
242+
const snapshot = await profileRef.once('value');
243+
const profileExists = snapshot.exists();
244+
207245
// Save the access token to the Firebase Realtime Database.
208246
const databaseTask = admin
209247
.database()
210248
.ref(`v2/OSMAccessToken/${uid}`)
211249
.set(accessToken);
212250

213-
const profileTask = admin
214-
.database()
215-
.ref(`v2/users/${uid}/`)
216-
.set({
217-
created: new Date().toISOString(),
218-
groupContributionCount: 0,
219-
projectContributionCount: 0,
220-
taskContributionCount: 0,
221-
displayName,
222-
});
223-
224251
// Create or update the firebase user account.
225252
// This does not login the user on the app, it just ensures that a firebase
226253
// user account (linked to the OSM account) exists.
@@ -240,8 +267,27 @@ async function createFirebaseAccount(admin: any, osmID: any, displayName: any, a
240267
throw error;
241268
});
242269

270+
// If profile exists, only update displayName -- else create new user profile
271+
const tasks = [userCreationTask, databaseTask];
272+
if (profileExists) {
273+
functions.logger.log('Sign in to existing OSM profile');
274+
const profileUpdateTask = profileRef.update({ displayName: displayName });
275+
tasks.push(profileUpdateTask);
276+
} else {
277+
functions.logger.log('Sign up new OSM profile');
278+
const profileCreationTask = profileRef
279+
.set({
280+
created: new Date().toISOString(),
281+
groupContributionCount: 0,
282+
projectContributionCount: 0,
283+
taskContributionCount: 0,
284+
displayName,
285+
});
286+
tasks.push(profileCreationTask);
287+
}
288+
243289
// Wait for all async task to complete then generate and return a custom auth token.
244-
await Promise.all([userCreationTask, databaseTask, profileTask]);
290+
await Promise.all(tasks);
245291
// Create a Firebase custom auth token.
246292
functions.logger.log('In createFirebaseAccount: createCustomToken');
247293
let authToken;

0 commit comments

Comments
 (0)