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)
2222const OAUTH_REDIRECT_URI = functions . config ( ) . osm ?. redirect_uri ;
23+ const OAUTH_REDIRECT_URI_WEB = functions . config ( ) . osm ?. redirect_web ;
2324
2425const 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.
@@ -51,6 +53,21 @@ function osmOAuth2Client() {
5153 return simpleOAuth2 . create ( credentials ) ;
5254}
5355
56+ function osmOAuth2ClientWeb ( ) {
57+ const credentials = {
58+ client : {
59+ id : functions . config ( ) . osm ?. client_id_web ,
60+ secret : functions . config ( ) . osm ?. client_secret_web ,
61+ } ,
62+ auth : {
63+ tokenHost : OSM_API_URL ,
64+ tokenPath : '/oauth2/token' ,
65+ authorizePath : '/oauth2/authorize' ,
66+ } ,
67+ } ;
68+ return simpleOAuth2 . create ( credentials ) ;
69+ }
70+
5471/**
5572 * Redirects the User to the OSM authentication consent screen.
5673 * Also the '__session' cookie is set for later state verification.
@@ -84,6 +101,32 @@ export const redirect = (req: any, res: any) => {
84101 } ) ;
85102} ;
86103
104+ export const redirect_web = ( req : any , res : any ) => {
105+ const oauth2 = osmOAuth2ClientWeb ( ) ;
106+
107+ cookieParser ( ) ( req , res , ( ) => {
108+ const state =
109+ req . cookies . state || crypto . randomBytes ( 20 ) . toString ( 'hex' ) ;
110+ functions . logger . log ( 'Setting verification state:' , state ) ;
111+ // the cookie MUST be called __session for hosted functions not to
112+ // strip it from incoming requests
113+ // (https://firebase.google.com/docs/hosting/manage-cache#using_cookies)
114+ res . cookie ( '__session' , state . toString ( ) , {
115+ // cookie is valid for 1 hour
116+ maxAge : 3600000 ,
117+ secure : true ,
118+ httpOnly : true ,
119+ } ) ;
120+ const redirectUri = oauth2 . authorizationCode . authorizeURL ( {
121+ redirect_uri : OAUTH_REDIRECT_URI_WEB ,
122+ scope : OAUTH_SCOPES ,
123+ state : state ,
124+ } ) ;
125+ functions . logger . log ( 'Redirecting to:' , redirectUri ) ;
126+ res . redirect ( redirectUri ) ;
127+ } ) ;
128+ } ;
129+
87130/**
88131 * The OSM OAuth endpoing does not give us any info about the user,
89132 * so we need to get the user profile from this endpoint
@@ -189,6 +232,89 @@ export const token = async (req: any, res: any, admin: any) => {
189232 }
190233} ;
191234
235+
236+ export const token_web = async ( req : any , res : any , admin : any ) => {
237+ const oauth2 = osmOAuth2ClientWeb ( ) ;
238+
239+ try {
240+ return cookieParser ( ) ( req , res , async ( ) => {
241+ functions . logger . log (
242+ 'Received verification state:' ,
243+ req . cookies . __session ,
244+ ) ;
245+ functions . logger . log ( 'Received state:' , req . query . state ) ;
246+ // FIXME: For security, we need to check the cookie that was set
247+ // in the /redirect_web function on the user's browser.
248+ // However, there seems to be a bug in firebase around this.
249+ // https://github.com/firebase/firebase-functions/issues/544
250+ // and linked SO question
251+ // firebase docs mention the need for a cookie middleware, but there
252+ // is no info about it :(
253+ // cross site cookies don't seem to be the issue
254+ // WE just need to make sure the domain set on the cookies is right
255+ if ( ! req . cookies . __session ) {
256+ throw new Error ( 'State cookie not set or expired. Maybe you took too long to authorize. Please try again.' ) ;
257+ } else if ( req . cookies . __session !== req . query . state ) {
258+ throw new Error ( 'State validation failed' ) ;
259+ }
260+ functions . logger . log ( 'Received auth code:' , req . query . code ) ;
261+ let results ;
262+
263+ try {
264+ // TODO: try adding auth data to request headers if
265+ // this doesn't work
266+ results = await oauth2 . authorizationCode . getToken ( {
267+ code : req . query . code ,
268+ redirect_uri : OAUTH_REDIRECT_URI ,
269+ scope : OAUTH_SCOPES ,
270+ state : req . query . state ,
271+ } ) ;
272+ } catch ( error : any ) {
273+ functions . logger . log ( 'Auth token error' , error , error . data . res . req ) ;
274+ }
275+ // why is token called twice?
276+ functions . logger . log (
277+ 'Auth code exchange result received:' ,
278+ results ,
279+ ) ;
280+
281+ // We have an OSM access token and the user identity now.
282+ const accessToken = results && results . access_token ;
283+ if ( accessToken === undefined ) {
284+ throw new Error (
285+ 'Could not get an access token from OpenStreetMap' ,
286+ ) ;
287+ }
288+ // get the OSM user id and display_name
289+ const { id, display_name } = await getOSMProfile ( accessToken ) ;
290+ functions . logger . log ( 'osmuser:' , id , display_name ) ;
291+ if ( id === undefined ) {
292+ // this should not happen, but help guard against creating
293+ // invalid accounts
294+ throw new Error ( 'Could not obtain an account id from OSM' ) ;
295+ }
296+
297+ // Create a Firebase account and get the Custom Auth Token.
298+ const firebaseToken = await createFirebaseAccount (
299+ admin ,
300+ id ,
301+ display_name ,
302+ accessToken ,
303+ ) ;
304+ // build a deep link so we can send the token back to the app
305+ // from the browser
306+ const signinUrl = `${ APP_OSM_LOGIN_DEEPLINK_WEB } ?token=${ firebaseToken } ` ;
307+ functions . logger . log ( 'redirecting user to' , signinUrl ) ;
308+ res . redirect ( signinUrl ) ;
309+ } ) ;
310+ } catch ( error : any ) {
311+ // FIXME: this should show up in the user's browser as a bit of text
312+ // We should figure out the various error codes available and feed them
313+ // back into the app to allow the user to take action
314+ return res . json ( { error : error . toString ( ) } ) ;
315+ }
316+ } ;
317+
192318/**
193319 * Creates a Firebase account with the given user profile and returns a custom
194320 * auth token allowing the user to sign in to this account on the app.
0 commit comments