diff --git a/README.md b/README.md index 44a0613e7b..216e3f56f2 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ -# Firebase Cloud Functions Templates Library +# Firebase Functions Samples Library -This repository contains a collection of templates showcasing some typical uses of Firebase Cloud Functions. +This repository contains a collection of samples showcasing some typical uses of Firebase Functions. ## Prerequisites -To learn how to get started with Cloud Functions and Firebase try the [quickstart](https://devrel.git.corp.google.com/samples/firebase/quickstart/functions/) and have a look at [the documentation](https://firebase.google.com/preview/functions/). +To learn how to get started with Firebase Functions try the [quickstart samples](https://devrel.git.corp.google.com/samples/firebase/quickstart/functions/) and have a look at [the documentation](https://firebase.google.com/preview/functions/). ## Use Cases and Samples -This repository contains the following templates: +This repository contains the following samples: + +### [Authorize with LinkedIn](/linkedin-auth) + +Demonstrates how to authorize with a 3rd party sign-in mechanism (LinkedIn in this case), create a Firebase custom auth token, update the user's profile and authorize Firebase. ### [Text Moderation](/text-moderation) diff --git a/instagram-auth/README.md b/instagram-auth/README.md new file mode 100644 index 0000000000..90b673dc7b --- /dev/null +++ b/instagram-auth/README.md @@ -0,0 +1,63 @@ +# Use LinkedIn Sign In with Firebase + +This sample shows how to authenticate using LinkedIn Sign-In on Firebase. In this sample we use OAuth 2.0 based +authentication to get LinkedIn user information then create a Firebase Custom Token (using the LinkedIn user ID). + + +## Setup the sample + +Create and setup the Firebase project: + 1. Create a Firebase project using the [Firebase Developer Console](https://console.firebase.google.com). + 1. Enable Billing on your Firebase the project by switching to the **Blaze** plan, this is currently needed for + Firebase Functions. + 1. Copy the Web initialisation snippet from **Firebase Console > Overview > Add Firebase to your web app** and paste it + in `public/index.html` and `public/popup.html` in lieu of the placeholders (where the `TODO(DEVELOPER)` + are located). + 1. From Firebse initialization snippet copy the `apiKey` value and paste it in `env.json` for the attribute + `firebaseConfig.apiKey` in lieu of the placeholder. + +Create and provide a Service Account's keys: + 1. Create a Service Accounts file as described in the [Server SDK setup instructions](https://firebase.google.com/docs/server/setup#add_firebase_to_your_app). + 1. Save the Service Account credential file as `./functions/service-account.json` + + +Create and setup your LinkedIn app: + 1. Create a LinkedIn app in the [LinkedIn Developers website](https://www.linkedin.com/developer/apps/). + 1. Add the URL `https://.firebaseapp.com/popup.html` to the + **OAuth 2.0** > **Authorized Redirect URLs** of your LinkedIn app. + 1. Copy the **Client ID** and **Client Secret** of your LinkedIn app and paste them in `env.json` for the attribute + `linkedIn.clientId` and `linkedIn.secret` in lieu of the placeholders. + + > Make sure the LinkedIn Client Secret is always kept secret. For instance do not save this in your version control system. + +Deploy your project: + 1. Run `firebase use --add` and choose your Firebase project. This will configure the Firebase CLI to use the correct + project locally. + 1. Run `firebase deploy` to effectively deploy the sample. The first time the Functions are deployed the process can + take several minutes. + + +## Run the sample + +Open the sample's website by using `firebase open hosting:site` or directly accessing `https://.firebaseapp.com/`. + +Click on the **Sign in with LinkedIn** button and a popup window will appear that will show the Linked In authentication consent screen. Sign In and/or authorize the authentication request. + +The website should display your name, email and profile pic from Linked In. At this point you are authenticated in Firebase and can use the database/hosting etc... + +## Workflow and design + +When Clicking the **Sign in with LinkedIn** button a popup is shown which redirects users to the `redirect` Function URL. + +The `redirect` Function then redirects the user to the LinkedIn OAuth 2.0 consent screen where (the first time only) the user will have to grant approval. Also the `state` cookie is set on the client with the value of the `state` URL query parameter to check against later on. + +After the user has granted approval he is redirected back to the `./popup.html` page along with an OAuth 2.0 Auth Code as a URL parameter. This Auth code is then sent to the `token` Function using a JSONP Request. The `token` function then: + - Checks that the value of the `state` URL query parameter is the same as the one in the `state` cookie. + - Exchanges the auth code for an access token using the LinkedIn app credentials. + - Use the Access Token to query the LinkedIn API to get user's information such as ID, name, email and profile pic URL. + - Mints a Custom Auth token (which is why we need Service Accounts Credentials). + - Use the Custom Auth token to authorize as the user and updates the email and/or profile information on Firebase if needed. + - Returns the Custom Auth Token to the `./popup.html` page. + + The `./popup.html` receives the Custom Auth Token back from the AJAX request to the `token` Function and uses it to authenticate the user in Firebase. Then close the popup. + At this point the main page will detect the sign-in through the Firebase Auth State observer and display the signed-In user information. \ No newline at end of file diff --git a/instagram-auth/env.json b/instagram-auth/env.json new file mode 100644 index 0000000000..959681aff9 --- /dev/null +++ b/instagram-auth/env.json @@ -0,0 +1,9 @@ +{ + "instagram": { + "clientId": "YOUR_INSTAGRAM_APP_CLIENT_ID", + "clientSecret": "YOUR_INSTAGRAM_APP_CLIENT_SECRET" + }, + "firebaseConfig": { + "apiKey": "YOUR_FIREBASE_PROJECT_API_KEY" + } +} diff --git a/instagram-auth/firebase.json b/instagram-auth/firebase.json new file mode 100644 index 0000000000..9c1c0c967a --- /dev/null +++ b/instagram-auth/firebase.json @@ -0,0 +1,5 @@ +{ + "hosting": { + "public": "public" + } +} diff --git a/instagram-auth/functions/index.js b/instagram-auth/functions/index.js new file mode 100644 index 0000000000..ab7cd5a41a --- /dev/null +++ b/instagram-auth/functions/index.js @@ -0,0 +1,131 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for t`he specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +const functions = require('firebase-functions'); +const cookieParser = require('cookie-parser'); +const crypto = require('crypto'); +const firebase = require('firebase'); +firebase.initializeApp({ + serviceAccount: require('./service-account.json'), + databaseURL: 'https://' + process.env.GCLOUD_PROJECT + '.firebaseio.com' +}); +const oauth2 = require('simple-oauth2')({ + clientID: functions.env.get('instagram.clientId'), + clientSecret: functions.env.get('instagram.clientSecret'), + site: 'https://api.instagram.com', + tokenPath: '/oauth/access_token', + authorizationPath: '/oauth/authorize' +}); + +const OAUTH_REDIRECT_URI = 'https://' + process.env.GCLOUD_PROJECT + '.firebaseapp.com/popup.html'; +const OAUTH_SCOPES = 'basic'; + +/** + * Redirects the User to the Instagram authentication consent screen. Also the 'state' cookie is set for later state + * verification. + */ +exports.redirect = functions.cloud.http().on('request', (req, res) => { + cookieParser()(req, res, () => { + try { + const state = req.cookies.state || crypto.randomBytes(20).toString('hex'); + console.log('Setting verification state:', state); + res.cookie('state', state.toString(), {maxAge: 3600000, secure: true, httpOnly: true}); + const redirectUri = oauth2.authCode.authorizeURL({ + redirect_uri: OAUTH_REDIRECT_URI, + scope: OAUTH_SCOPES, + state: state + }); + console.log('Redirecting to:', redirectUri); + res.redirect(redirectUri); + } catch (e) { + res.status(500).send(e.toString()); + } + }); +}); + +/** + * Exchanges a given Instagram auth code passed in the 'code' URL query parameter for a Firebase auth token. + * The request also needs to specify a 'state' query parameter which will be checked against the 'state' cookie. + * The Firebase custom auth token is sent back in a JSONP callback function with function name defined by the + * 'callback' query parameter. + */ +exports.token = functions.cloud.http().on('request', (req, res) => { + try { + cookieParser()(req, res, () => { + console.log('Received verification state:', req.cookies.state); + console.log('Received state:', req.query.state); + if (!req.cookies.state) { + throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.'); + } else if (req.cookies.state !== req.query.state) { + throw new Error('State validation failed'); + } + console.log('Received auth code:', req.query.code); + oauth2.authCode.getToken({ + code: req.query.code, + redirect_uri: OAUTH_REDIRECT_URI + }).then(results => { + console.log('Auth code exchange result received:', results); + const firebaseAccount = createFirebaseToken(results.user.id); + return updateAccount(firebaseAccount.token, firebaseAccount.uid, + results.user.full_name, results.user.profile_picture) + .then(() => res.jsonp({token: firebaseAccount.token})); + }); + }); + } catch (error) { + return res.jsonp({error: error.toString}); + } +}); + +/** + * Creates a Firebase custom auth token for the given Instagram user ID. + * + * @returns {Object} The Firebase custom auth token and the uid. + */ +function createFirebaseToken(digitsUID) { + // The UID we'll assign to the user. + const uid = `instagram:${digitsUID}`; + + // Create the custom token. + const token = firebase.app().auth().createCustomToken(uid); + console.log('Created Custom token for UID "', uid, '" Token:', token); + return {token: token, uid: uid}; +} + +/** + * Updates the user with the given displayName and photoURL. Updates the Firebase user profile with the + * displayName if needed. + * + * @returns {Promise} Promise that completes when all the updates have been completed. + */ +function updateAccount(token, uid, displayName, photoURL) { + // Create a Firebase app we'll use to authenticate as the user. + const userApp = firebase.initializeApp({ + apiKey: functions.env.get('firebaseConfig.apiKey') + }, uid); + + // Authenticate as the user, updates the email and profile Pic. + return userApp.auth().signInWithCustomToken(token).then(user => { + if (displayName !== user.displayName || photoURL !== user.photoURL) { + console.log('Updating profile of user', uid, 'with', {displayName: displayName, photoURL: photoURL}); + return user.updateProfile({displayName: displayName, photoURL: photoURL}).then(() => userApp.delete()); + } + return userApp.delete(); + }).catch(e => { + userApp.delete(); + throw e; + }); +} diff --git a/instagram-auth/functions/package.json b/instagram-auth/functions/package.json new file mode 100644 index 0000000000..084dfe378c --- /dev/null +++ b/instagram-auth/functions/package.json @@ -0,0 +1,13 @@ +{ + "name": "functions", + "description": "Firebase Functions", + "dependencies": { + "cookie-parser": "^1.4.3", + "crypto": "0.0.3", + "firebase": "^3.3.0", + "firebase-functions": "https://storage.googleapis.com/firebase-preview-drop/node/firebase-functions/firebase-functions-preview.latest.tar.gz", + "request": "^2.74.0", + "request-promise-native": "^1.0.3", + "simple-oauth2": "^0.8.0" + } +} diff --git a/instagram-auth/main.css b/instagram-auth/main.css new file mode 100644 index 0000000000..a1afdd12a5 --- /dev/null +++ b/instagram-auth/main.css @@ -0,0 +1,62 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +html, body { + font-family: 'Roboto', 'Helvetica', sans-serif; +} +.mdl-grid { + max-width: 1024px; + margin: auto; +} +.mdl-card { + min-height: 0; + padding-bottom: 5px; +} +.mdl-layout__header-row { + padding: 0; +} +#message-form { + display: flex; + flex-direction: column; +} +#message-form button { + max-width: 300px; +} +#message-list { + padding: 0; + width: 100%; +} +#message-list > div { + padding: 15px; + border-bottom: 1px #f1f1f1 solid; +} +h3 { + background: url('firebase-logo.png') no-repeat; + background-size: 40px; + padding-left: 50px; +} +#demo-signed-out-card, +#demo-signed-in-card, +#demo-subscribe-button, +#demo-unsubscribe-button, +#demo-subscribed-text-container, +#demo-unsubscribed-text-container { + display: none; +} +#demo-subscribe-button, +#demo-unsubscribe-button { + margin-right: 20px; +} \ No newline at end of file diff --git a/instagram-auth/public/firebase-logo.png b/instagram-auth/public/firebase-logo.png new file mode 100644 index 0000000000..c498b958bb Binary files /dev/null and b/instagram-auth/public/firebase-logo.png differ diff --git a/instagram-auth/public/index.html b/instagram-auth/public/index.html new file mode 100644 index 0000000000..1401acd5ed --- /dev/null +++ b/instagram-auth/public/index.html @@ -0,0 +1,77 @@ + + + + + + + + + Firebase Functions demo to Sign In with Instagram + + + + + + + + + +
+ + +
+
+
+

Sign in with Instagram demo

+
+
+
+
+
+ + +
+
+

+ This web application demonstrates how you can Sign In with Instagram to Firebase Authentication. + Now sign in! +

+ +
+
+ + +
+
+

+ Welcome
+ Your Firebase User ID is:
+ Your profile picture: +

+ + +
+
+
+
+
+ + + + + + + diff --git a/instagram-auth/public/main.css b/instagram-auth/public/main.css new file mode 100644 index 0000000000..b9d21d0491 --- /dev/null +++ b/instagram-auth/public/main.css @@ -0,0 +1,54 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +html, body { + font-family: 'Roboto', 'Helvetica', sans-serif; +} +.mdl-grid { + max-width: 1024px; + margin: auto; +} +.mdl-card { + min-height: 0; + padding-bottom: 5px; +} +.mdl-layout__header-row { + padding: 0; +} +h3 { + background: url('firebase-logo.png') no-repeat; + background-size: 40px; + padding-left: 50px; +} +#demo-signed-out-card, +#demo-signed-in-card { + display: none; +} +#demo-profile-pic { + height: 60px; + width: 60px; + border-radius: 30px; + margin-left: calc(50% - 30px); + margin-top: 10px; +} +#demo-name-container, +#demo-email-container, +#demo-uid-container { + font-weight: bold; +} +#demo-delete-button { + margin-left: 20px; +} diff --git a/instagram-auth/public/main.js b/instagram-auth/public/main.js new file mode 100644 index 0000000000..6fbe8e87a1 --- /dev/null +++ b/instagram-auth/public/main.js @@ -0,0 +1,85 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +// Initializes the Demo. +function Demo() { + document.addEventListener('DOMContentLoaded', function() { + // Shortcuts to DOM Elements. + this.signInButton = document.getElementById('demo-sign-in-button'); + this.signOutButton = document.getElementById('demo-sign-out-button'); + this.nameContainer = document.getElementById('demo-name-container'); + this.uidContainer = document.getElementById('demo-uid-container'); + this.deleteButton = document.getElementById('demo-delete-button'); + this.profilePic = document.getElementById('demo-profile-pic'); + this.signedOutCard = document.getElementById('demo-signed-out-card'); + this.signedInCard = document.getElementById('demo-signed-in-card'); + + // Bind events. + this.signInButton.addEventListener('click', this.signIn.bind(this)); + this.signOutButton.addEventListener('click', this.signOut.bind(this)); + this.deleteButton.addEventListener('click', this.deleteAccount.bind(this)); + firebase.auth().onAuthStateChanged(this.onAuthStateChanged.bind(this)); + }.bind(this)); +} + +// Triggered on Firebase auth state change. +Demo.prototype.onAuthStateChanged = function(user) { + if (user) { + this.nameContainer.innerText = user.displayName; + this.uidContainer.innerText = user.uid; + this.profilePic.src = user.photoURL; + this.signedOutCard.style.display = 'none'; + this.signedInCard.style.display = 'block'; + } else { + this.signedOutCard.style.display = 'block'; + this.signedInCard.style.display = 'none'; + } +}; + +// Initiates the sign-in flow using LinkedIn sign in in a popup. +Demo.prototype.signIn = function() { + // This is the URL to the HTTP triggered 'redirect' Firebase Function. + // See https://firebase.google.com/preview/functions/gcp-events#handle_a_cloud_http_firebase_function_event. + var redirectFunctionURL = 'https://us-central1-' + Demo.getFirebaseProjectId() + '.cloudfunctions.net/redirect'; + // Open the Functions URL as a popup. + window.open(redirectFunctionURL, 'name', 'height=585,width=400'); +}; + +// Signs-out of Firebase. +Demo.prototype.signOut = function() { + firebase.auth().signOut(); +}; + +// Deletes the user's account. +Demo.prototype.deleteAccount = function() { + firebase.auth().currentUser.delete().then(function() { + window.alert('Account deleted'); + }).catch(function(error) { + if (error.code === 'auth/requires-recent-login') { + window.alert('You need to have recently signed-in to delete your account. Please sign-in and try again.'); + firebase.auth().signOut(); + } + }); +}; + +// Returns the Firebase project ID of the default Firebase app. +Demo.getFirebaseProjectId = function() { + return firebase.app().options.authDomain.split('.')[0]; +}; + +// Load the demo. +new Demo(); diff --git a/instagram-auth/public/popup.html b/instagram-auth/public/popup.html new file mode 100644 index 0000000000..02400783da --- /dev/null +++ b/instagram-auth/public/popup.html @@ -0,0 +1,84 @@ + + + + + + + + + Authenticate with Instagram + + + +Please wait... + + + + + + + diff --git a/linkedin-auth/README.md b/linkedin-auth/README.md new file mode 100644 index 0000000000..90b673dc7b --- /dev/null +++ b/linkedin-auth/README.md @@ -0,0 +1,63 @@ +# Use LinkedIn Sign In with Firebase + +This sample shows how to authenticate using LinkedIn Sign-In on Firebase. In this sample we use OAuth 2.0 based +authentication to get LinkedIn user information then create a Firebase Custom Token (using the LinkedIn user ID). + + +## Setup the sample + +Create and setup the Firebase project: + 1. Create a Firebase project using the [Firebase Developer Console](https://console.firebase.google.com). + 1. Enable Billing on your Firebase the project by switching to the **Blaze** plan, this is currently needed for + Firebase Functions. + 1. Copy the Web initialisation snippet from **Firebase Console > Overview > Add Firebase to your web app** and paste it + in `public/index.html` and `public/popup.html` in lieu of the placeholders (where the `TODO(DEVELOPER)` + are located). + 1. From Firebse initialization snippet copy the `apiKey` value and paste it in `env.json` for the attribute + `firebaseConfig.apiKey` in lieu of the placeholder. + +Create and provide a Service Account's keys: + 1. Create a Service Accounts file as described in the [Server SDK setup instructions](https://firebase.google.com/docs/server/setup#add_firebase_to_your_app). + 1. Save the Service Account credential file as `./functions/service-account.json` + + +Create and setup your LinkedIn app: + 1. Create a LinkedIn app in the [LinkedIn Developers website](https://www.linkedin.com/developer/apps/). + 1. Add the URL `https://.firebaseapp.com/popup.html` to the + **OAuth 2.0** > **Authorized Redirect URLs** of your LinkedIn app. + 1. Copy the **Client ID** and **Client Secret** of your LinkedIn app and paste them in `env.json` for the attribute + `linkedIn.clientId` and `linkedIn.secret` in lieu of the placeholders. + + > Make sure the LinkedIn Client Secret is always kept secret. For instance do not save this in your version control system. + +Deploy your project: + 1. Run `firebase use --add` and choose your Firebase project. This will configure the Firebase CLI to use the correct + project locally. + 1. Run `firebase deploy` to effectively deploy the sample. The first time the Functions are deployed the process can + take several minutes. + + +## Run the sample + +Open the sample's website by using `firebase open hosting:site` or directly accessing `https://.firebaseapp.com/`. + +Click on the **Sign in with LinkedIn** button and a popup window will appear that will show the Linked In authentication consent screen. Sign In and/or authorize the authentication request. + +The website should display your name, email and profile pic from Linked In. At this point you are authenticated in Firebase and can use the database/hosting etc... + +## Workflow and design + +When Clicking the **Sign in with LinkedIn** button a popup is shown which redirects users to the `redirect` Function URL. + +The `redirect` Function then redirects the user to the LinkedIn OAuth 2.0 consent screen where (the first time only) the user will have to grant approval. Also the `state` cookie is set on the client with the value of the `state` URL query parameter to check against later on. + +After the user has granted approval he is redirected back to the `./popup.html` page along with an OAuth 2.0 Auth Code as a URL parameter. This Auth code is then sent to the `token` Function using a JSONP Request. The `token` function then: + - Checks that the value of the `state` URL query parameter is the same as the one in the `state` cookie. + - Exchanges the auth code for an access token using the LinkedIn app credentials. + - Use the Access Token to query the LinkedIn API to get user's information such as ID, name, email and profile pic URL. + - Mints a Custom Auth token (which is why we need Service Accounts Credentials). + - Use the Custom Auth token to authorize as the user and updates the email and/or profile information on Firebase if needed. + - Returns the Custom Auth Token to the `./popup.html` page. + + The `./popup.html` receives the Custom Auth Token back from the AJAX request to the `token` Function and uses it to authenticate the user in Firebase. Then close the popup. + At this point the main page will detect the sign-in through the Firebase Auth State observer and display the signed-In user information. \ No newline at end of file diff --git a/linkedin-auth/env.json b/linkedin-auth/env.json new file mode 100644 index 0000000000..e83d20c1f8 --- /dev/null +++ b/linkedin-auth/env.json @@ -0,0 +1,9 @@ +{ + "linkedIn": { + "clientId": "YOUR_LINKEDIN_APP_CLIENT_ID", + "clientSecret": "YOUR_LINKEDIN_APP_CLIENT_SECRET" + }, + "firebaseConfig": { + "apiKey": "YOUR_FIREBASE_PROJECT_API_KEY" + } +} diff --git a/linkedin-auth/firebase.json b/linkedin-auth/firebase.json new file mode 100644 index 0000000000..9c1c0c967a --- /dev/null +++ b/linkedin-auth/firebase.json @@ -0,0 +1,5 @@ +{ + "hosting": { + "public": "public" + } +} diff --git a/linkedin-auth/functions/index.js b/linkedin-auth/functions/index.js new file mode 100644 index 0000000000..da4772c253 --- /dev/null +++ b/linkedin-auth/functions/index.js @@ -0,0 +1,138 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for t`he specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +const functions = require('firebase-functions'); +const cookieParser = require('cookie-parser'); +const crypto = require('crypto'); +const firebase = require('firebase'); +firebase.initializeApp({ + serviceAccount: require('./service-account.json'), + databaseURL: 'https://' + process.env.GCLOUD_PROJECT + '.firebaseio.com' +}); +const Linkedin = require('node-linkedin')( + functions.env.get('linkedIn.clientId'), + functions.env.get('linkedIn.clientSecret'), + 'https://' + process.env.GCLOUD_PROJECT + '.firebaseapp.com/popup.html'); + +const OAUTH_SCOPES = ['r_basicprofile', 'r_emailaddress']; + +/** + * Redirects the User to the LinkedIn authentication consent screen. ALso the 'state' cookie is set for later state + * verification. + */ +exports.redirect = functions.cloud.http().on('request', (req, res) => { + cookieParser()(req, res, () => { + try { + const state = req.cookies.state || crypto.randomBytes(20).toString('hex'); + console.log('Setting verification state:', state); + res.cookie('state', state.toString(), {maxAge: 3600000, secure: true, httpOnly: true}); + Linkedin.auth.authorize(res, OAUTH_SCOPES, state.toString()); + } catch (e) { + res.status(500).send(e.toString()); + } + }); +}); + +/** + * Exchanges a given LinkedIn auth code passed in the 'code' URL query parameter for a Firebase auth token. + * The request also needs to specify a 'state' query parameter which will be checked against the 'state' cookie. + * The Firebase custom auth token is sent back in a JSONP callback function with function name defined by the + * 'callback' query parameter. + */ +exports.token = functions.cloud.http().on('request', (req, res) => { + try { + cookieParser()(req, res, () => { + if (!req.cookies.state) { + throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.'); + } + console.log('Received verification state:', req.cookies.state); + Linkedin.auth.authorize(OAUTH_SCOPES, req.cookies.state); // Makes sure the state parameter is set + console.log('Received auth code:', req.query.code); + console.log('Received state:', req.query.state); + Linkedin.auth.getAccessToken(res, req.query.code, req.query.state, (error, results) => { + if (error) { + throw error; + } + console.log('Received Access Token:', results.access_token); + const linkedin = Linkedin.init(results.access_token); + linkedin.people.me((error, results) => { + if (error) { + throw error; + } + console.log('Auth code exchange result received:', results); + const firebaseAccount = createFirebaseToken(results.id); + return updateAccount(firebaseAccount.token, firebaseAccount.uid, + results.emailAddress, results.formattedName, results.pictureUrl) + .then(() => res.jsonp({token: firebaseAccount.token})); + }); + }); + }); + } catch (e) { + return res.jsonp({error: e.toString}); + } +}); + +/** + * Creates a Firebase custom auth token for the given Instagram user ID. + * + * @returns {Object} The Firebase custom auth token and the uid. + */ +function createFirebaseToken(digitsUID) { + // The UID we'll assign to the user. + const uid = `linkedin:${digitsUID}`; + + // Create the custom token. + const token = firebase.app().auth().createCustomToken(uid); + console.log('Created Custom token for UID "', uid, '" Token:', token); + return {token: token, uid: uid}; +} + +/** + * Updates the user with the given displayName and photoURL. Updates the Firebase user profile with the + * displayName if needed. + * + * @returns {Promise} Promise that completes when all the updates have been completed. + */ +function updateAccount(token, uid, email, displayName, photoURL) { + // Create a Firebase app we'll use to authenticate as the user. + const userApp = firebase.initializeApp({ + apiKey: functions.env.get('firebaseConfig.apiKey') + }, uid); + + // Update the profile of the user if needed. + const updateUserProfile = user => { + if (displayName !== user.displayName || photoURL !== user.photoURL) { + console.log('Updating profile of user', uid, 'with', {displayName: displayName, photoURL: photoURL}); + return user.updateProfile({displayName: displayName, photoURL: photoURL}).then(() => userApp.delete()); + } + return userApp.delete(); + }; + + // Authenticate as the user and updates the email, displayName and profilePic. + return userApp.auth().signInWithCustomToken(token).then(user => { + if (email !== user.email) { + console.log('Updating email of user', uid, 'with', email); + return user.updateEmail(email).then(() => updateUserProfile(user)); + } + return updateUserProfile(user); + }).catch(e => { + userApp.delete(); + throw e; + }); +} + + diff --git a/linkedin-auth/functions/package.json b/linkedin-auth/functions/package.json new file mode 100644 index 0000000000..d949477c42 --- /dev/null +++ b/linkedin-auth/functions/package.json @@ -0,0 +1,11 @@ +{ + "name": "functions", + "description": "Firebase Functions", + "dependencies": { + "cookie-parser": "^1.4.3", + "crypto": "0.0.3", + "firebase": "^3.3.0", + "firebase-functions": "https://storage.googleapis.com/firebase-preview-drop/node/firebase-functions/firebase-functions-preview.latest.tar.gz", + "node-linkedin": "^0.5.4" + } +} diff --git a/linkedin-auth/main.css b/linkedin-auth/main.css new file mode 100644 index 0000000000..a1afdd12a5 --- /dev/null +++ b/linkedin-auth/main.css @@ -0,0 +1,62 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +html, body { + font-family: 'Roboto', 'Helvetica', sans-serif; +} +.mdl-grid { + max-width: 1024px; + margin: auto; +} +.mdl-card { + min-height: 0; + padding-bottom: 5px; +} +.mdl-layout__header-row { + padding: 0; +} +#message-form { + display: flex; + flex-direction: column; +} +#message-form button { + max-width: 300px; +} +#message-list { + padding: 0; + width: 100%; +} +#message-list > div { + padding: 15px; + border-bottom: 1px #f1f1f1 solid; +} +h3 { + background: url('firebase-logo.png') no-repeat; + background-size: 40px; + padding-left: 50px; +} +#demo-signed-out-card, +#demo-signed-in-card, +#demo-subscribe-button, +#demo-unsubscribe-button, +#demo-subscribed-text-container, +#demo-unsubscribed-text-container { + display: none; +} +#demo-subscribe-button, +#demo-unsubscribe-button { + margin-right: 20px; +} \ No newline at end of file diff --git a/linkedin-auth/public/firebase-logo.png b/linkedin-auth/public/firebase-logo.png new file mode 100644 index 0000000000..c498b958bb Binary files /dev/null and b/linkedin-auth/public/firebase-logo.png differ diff --git a/linkedin-auth/public/index.html b/linkedin-auth/public/index.html new file mode 100644 index 0000000000..f904ac297e --- /dev/null +++ b/linkedin-auth/public/index.html @@ -0,0 +1,78 @@ + + + + + + + + + Firebase Functions demo to Sign In with LinkedIn + + + + + + + + + +
+ + +
+
+
+

Sign in with LinkedIn demo

+
+
+
+
+
+ + +
+
+

+ This web application demonstrates how you can Sign In with LinkedIn to Firebase Authentication. + Now sign in! +

+ +
+
+ + +
+
+

+ Welcome
+ Your email is:
+ Your Firebase User ID is:
+ Your profile picture: +

+ + +
+
+
+
+
+ + + + + + + diff --git a/linkedin-auth/public/linkedIn-button.png b/linkedin-auth/public/linkedIn-button.png new file mode 100644 index 0000000000..323f392ce7 Binary files /dev/null and b/linkedin-auth/public/linkedIn-button.png differ diff --git a/linkedin-auth/public/main.css b/linkedin-auth/public/main.css new file mode 100644 index 0000000000..09ec65b048 --- /dev/null +++ b/linkedin-auth/public/main.css @@ -0,0 +1,60 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +html, body { + font-family: 'Roboto', 'Helvetica', sans-serif; +} +.mdl-grid { + max-width: 1024px; + margin: auto; +} +.mdl-card { + min-height: 0; + padding-bottom: 5px; +} +.mdl-layout__header-row { + padding: 0; +} +h3 { + background: url('firebase-logo.png') no-repeat; + background-size: 40px; + padding-left: 50px; +} +#demo-signed-out-card, +#demo-signed-in-card { + display: none; +} +#demo-profile-pic { + height: 60px; + width: 60px; + border-radius: 30px; + margin-left: calc(50% - 30px); + margin-top: 10px; +} +#demo-name-container, +#demo-email-container, +#demo-uid-container { + font-weight: bold; +} +#demo-sign-in-button { + background-image: url('linkedIn-button.png'); + background-size: 100% 100%; + width: 197px; + height: 27px; +} +#demo-delete-button { + margin-left: 20px; +} diff --git a/linkedin-auth/public/main.js b/linkedin-auth/public/main.js new file mode 100644 index 0000000000..f3b283a05e --- /dev/null +++ b/linkedin-auth/public/main.js @@ -0,0 +1,87 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +// Initializes the Demo. +function Demo() { + document.addEventListener('DOMContentLoaded', function() { + // Shortcuts to DOM Elements. + this.signInButton = document.getElementById('demo-sign-in-button'); + this.signOutButton = document.getElementById('demo-sign-out-button'); + this.emailContainer = document.getElementById('demo-email-container'); + this.nameContainer = document.getElementById('demo-name-container'); + this.deleteButton = document.getElementById('demo-delete-button'); + this.uidContainer = document.getElementById('demo-uid-container'); + this.profilePic = document.getElementById('demo-profile-pic'); + this.signedOutCard = document.getElementById('demo-signed-out-card'); + this.signedInCard = document.getElementById('demo-signed-in-card'); + + // Bind events. + this.signInButton.addEventListener('click', this.signIn.bind(this)); + this.signOutButton.addEventListener('click', this.signOut.bind(this)); + this.deleteButton.addEventListener('click', this.deleteAccount.bind(this)); + firebase.auth().onAuthStateChanged(this.onAuthStateChanged.bind(this)); + }.bind(this)); +} + +// Triggered on Firebase auth state change. +Demo.prototype.onAuthStateChanged = function(user) { + if (user) { + this.nameContainer.innerText = user.displayName; + this.emailContainer.innerText = user.email; + this.uidContainer.innerText = user.uid; + this.profilePic.src = user.photoURL; + this.signedOutCard.style.display = 'none'; + this.signedInCard.style.display = 'block'; + } else { + this.signedOutCard.style.display = 'block'; + this.signedInCard.style.display = 'none'; + } +}; + +// Initiates the sign-in flow using LinkedIn sign in in a popup. +Demo.prototype.signIn = function() { + // This is the URL to the HTTP triggered 'redirect' Firebase Function. + // See https://firebase.google.com/preview/functions/gcp-events#handle_a_cloud_http_firebase_function_event. + var redirectFunctionURL = 'https://us-central1-' + Demo.getFirebaseProjectId() + '.cloudfunctions.net/redirect'; + // Open the Functions URL as a popup. + window.open(redirectFunctionURL, 'name', 'height=585,width=400'); +}; + +// Signs-out of Firebase. +Demo.prototype.signOut = function() { + firebase.auth().signOut(); +}; + +// Deletes the user's account. +Demo.prototype.deleteAccount = function() { + firebase.auth().currentUser.delete().then(function() { + window.alert('Account deleted'); + }).catch(function(error) { + if (error.code === 'auth/requires-recent-login') { + window.alert('You need to have recently signed-in to delete your account. Please sign-in and try again.'); + firebase.auth().signOut(); + } + }); +}; + +// Returns the Firebase project ID of the default Firebase app. +Demo.getFirebaseProjectId = function() { + return firebase.app().options.authDomain.split('.')[0]; +}; + +// Load the demo. +new Demo(); diff --git a/linkedin-auth/public/popup.html b/linkedin-auth/public/popup.html new file mode 100644 index 0000000000..52f7cf92ef --- /dev/null +++ b/linkedin-auth/public/popup.html @@ -0,0 +1,84 @@ + + + + + + + + + Authenticate With LinkedIn + + + +Please wait... + + + + + + +