Browse Source

Functions HTTP trigger sample: Authorize with LinkedIn and Instagram

Try it here: https://titanium-firebase-linkedin.firebaseapp.com/

Change-Id: I59575e26633ed6d9d4b07a79de6db6e78b44f609
ryanpbrewster-patch-1
Nicolas Garnier 8 years ago
parent
commit
79da0e6cd5
  1. 12
      README.md
  2. 63
      instagram-auth/README.md
  3. 9
      instagram-auth/env.json
  4. 5
      instagram-auth/firebase.json
  5. 131
      instagram-auth/functions/index.js
  6. 13
      instagram-auth/functions/package.json
  7. 62
      instagram-auth/main.css
  8. BIN
      instagram-auth/public/firebase-logo.png
  9. 77
      instagram-auth/public/index.html
  10. 54
      instagram-auth/public/main.css
  11. 85
      instagram-auth/public/main.js
  12. 84
      instagram-auth/public/popup.html
  13. 63
      linkedin-auth/README.md
  14. 9
      linkedin-auth/env.json
  15. 5
      linkedin-auth/firebase.json
  16. 138
      linkedin-auth/functions/index.js
  17. 11
      linkedin-auth/functions/package.json
  18. 62
      linkedin-auth/main.css
  19. BIN
      linkedin-auth/public/firebase-logo.png
  20. 78
      linkedin-auth/public/index.html
  21. BIN
      linkedin-auth/public/linkedIn-button.png
  22. 60
      linkedin-auth/public/main.css
  23. 87
      linkedin-auth/public/main.js
  24. 84
      linkedin-auth/public/popup.html

12
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)

63
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://<application-id>.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://<project-id>.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.

9
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"
}
}

5
instagram-auth/firebase.json

@ -0,0 +1,5 @@
{
"hosting": {
"public": "public"
}
}

131
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;
});
}

13
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"
}
}

62
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;
}

BIN
instagram-auth/public/firebase-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

77
instagram-auth/public/index.html

@ -0,0 +1,77 @@
<!doctype html>
<!--
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
https://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 lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="Demonstrates of to authorize Firebase with Instagram Auth using Firebase Functions">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firebase Functions demo to Sign In with Instagram</title>
<!-- Material Design Lite -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.blue_grey-orange.min.css">
<script defer src="https://code.getmdl.io/1.1.3/material.min.js"></script>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-header">
<!-- Header section containing title -->
<header class="mdl-layout__header mdl-color-text--white mdl-color--light-blue-700">
<div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid">
<div class="mdl-layout__header-row mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-cell--8-col-desktop">
<h3>Sign in with Instagram demo</h3>
</div>
</div>
</header>
<main class="mdl-layout__content mdl-color--grey-100">
<div class="mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid">
<!-- Card containing the sign-in UI -->
<div id="demo-signed-out-card" class="mdl-card mdl-shadow--2dp mdl-cell">
<div class="mdl-card__supporting-text mdl-color-text--grey-600">
<p>
This web application demonstrates how you can Sign In with Instagram to Firebase Authentication.
<strong>Now sign in!</strong>
</p>
<button id="demo-sign-in-button" class="mdl-color-text--grey-700 mdl-button--raised mdl-button mdl-js-button mdl-js-ripple-effect"><i class="material-icons">account_circle</i> Sign in with Instagram</button>
</div>
</div>
<!-- Card containing the signed-in UI -->
<div id="demo-signed-in-card" class="mdl-card mdl-shadow--2dp mdl-cell">
<div class="mdl-card__supporting-text mdl-color-text--grey-600">
<p>
Welcome <span id="demo-name-container"></span><br>
Your Firebase User ID is: <span id="demo-uid-container"></span><br>
Your profile picture: <img id="demo-profile-pic">
</p>
<button id="demo-sign-out-button" class="mdl-color-text--grey-700 mdl-button--raised mdl-button mdl-js-button mdl-js-ripple-effect">Sign out</button>
<button id="demo-delete-button" class="mdl-color-text--grey-700 mdl-button--raised mdl-button mdl-js-button mdl-js-ripple-effect">Delete account</button>
</div>
</div>
</div>
</main>
</div>
<!-- Firebase -->
<!-- ***********************************************************************************************************************
* TODO(DEVELOPER): Paste the initialization snippet from: Firebase Console > Overview > Add Firebase to your web app. *
*********************************************************************************************************************** -->
<script src="main.js"></script>
</body>
</html>

54
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;
}

85
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();

84
instagram-auth/public/popup.html

@ -0,0 +1,84 @@
<!doctype html>
<!--
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
https://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 lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="Demonstrates how to authorize Firebase with Instagram auth using Firebase Functions">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authenticate with Instagram</title>
</head>
<body>
Please wait...
<!-- Firebase -->
<!-- ***********************************************************************************************************************
* TODO(DEVELOPER): Paste the initialization snippet from: Firebase Console > Overview > Add Firebase to your web app. *
*********************************************************************************************************************** -->
<script>
/**
* Returns the value of the given URL query parameter.
*/
function getURLParameter(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) ||
[null, ''])[1].replace(/\+/g, '%20')) || null;
}
/**
* Returns the ID of the Firebase project.
*/
function getFirebaseProjectId() {
return firebase.app().options.authDomain.split('.')[0];
}
/**
* This callback is called by the JSONP callback of the 'token' Firebase Function with the Firebase auth token.
*/
function tokenReceived(data) {
if (data.token) {
firebase.auth().signInWithCustomToken(data.token).then(function() {
window.close();
}).catch(function(error) {
console.error(error);
document.body.innerText = 'Error While using the Firebase Auth token to sign in: ' + error.message;
})
} else {
console.error(error);
document.body.innerText = 'Error in the token Function: ' + data.error;
}
}
var code = getURLParameter('code');
var state = getURLParameter('state');
var error = getURLParameter('error');
if (!code) {
document.body.innerText = 'Error back from the Instagram auth page: ' + error;
} else {
// Use JSONP to load the 'token' Firebase Function to exchange the auth code against a Firebase custom token.
const script = document.createElement('script');
script.type = 'text/javascript';
// This is the URL to the HTTP triggered 'token' Firebase Function.
// See https://firebase.google.com/preview/functions/gcp-events#handle_a_cloud_http_firebase_function_event.
var tokenFunctionURL = 'https://us-central1-' + getFirebaseProjectId() + '.cloudfunctions.net/token';
script.src = tokenFunctionURL +
'?code=' + encodeURIComponent(code) +
'&state=' + encodeURIComponent(state) +
'&callback=' + tokenReceived.name;
document.head.appendChild(script);
}
</script>
</body>
</html>

63
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://<application-id>.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://<project-id>.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.

9
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"
}
}

5
linkedin-auth/firebase.json

@ -0,0 +1,5 @@
{
"hosting": {
"public": "public"
}
}

138
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;
});
}

11
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"
}
}

62
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;
}

BIN
linkedin-auth/public/firebase-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

78
linkedin-auth/public/index.html

@ -0,0 +1,78 @@
<!doctype html>
<!--
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
https://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 lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="Demonstrates of to authorize Firebase with LinkedIn auth using Firebase Functions">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firebase Functions demo to Sign In with LinkedIn</title>
<!-- Material Design Lite -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.blue_grey-orange.min.css">
<script defer src="https://code.getmdl.io/1.1.3/material.min.js"></script>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-header">
<!-- Header section containing title -->
<header class="mdl-layout__header mdl-color-text--white mdl-color--light-blue-700">
<div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid">
<div class="mdl-layout__header-row mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-cell--8-col-desktop">
<h3>Sign in with LinkedIn demo</h3>
</div>
</div>
</header>
<main class="mdl-layout__content mdl-color--grey-100">
<div class="mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid">
<!-- Card containing the sign-in UI -->
<div id="demo-signed-out-card" class="mdl-card mdl-shadow--2dp mdl-cell">
<div class="mdl-card__supporting-text mdl-color-text--grey-600">
<p>
This web application demonstrates how you can Sign In with LinkedIn to Firebase Authentication.
<strong>Now sign in!</strong>
</p>
<button id="demo-sign-in-button" class="mdl-color-text--grey-700 mdl-button--raised mdl-button mdl-js-button mdl-js-ripple-effect"></button>
</div>
</div>
<!-- Card containing the signed-in UI -->
<div id="demo-signed-in-card" class="mdl-card mdl-shadow--2dp mdl-cell">
<div class="mdl-card__supporting-text mdl-color-text--grey-600">
<p>
Welcome <span id="demo-name-container"></span><br>
Your email is: <span id="demo-email-container"></span><br>
Your Firebase User ID is: <span id="demo-uid-container"></span><br>
Your profile picture: <img id="demo-profile-pic">
</p>
<button id="demo-sign-out-button" class="mdl-color-text--grey-700 mdl-button--raised mdl-button mdl-js-button mdl-js-ripple-effect">Sign out</button>
<button id="demo-delete-button" class="mdl-color-text--grey-700 mdl-button--raised mdl-button mdl-js-button mdl-js-ripple-effect">Delete account</button>
</div>
</div>
</div>
</main>
</div>
<!-- Firebase -->
<!-- ***********************************************************************************************************************
* TODO(DEVELOPER): Paste the initialization snippet from: Firebase Console > Overview > Add Firebase to your web app. *
*********************************************************************************************************************** -->
<script src="main.js"></script>
</body>
</html>

BIN
linkedin-auth/public/linkedIn-button.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

60
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;
}

87
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();

84
linkedin-auth/public/popup.html

@ -0,0 +1,84 @@
<!doctype html>
<!--
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
https://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 lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="Demonstrates how to authenticate with LinkedIn on Firebase using Functions">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authenticate With LinkedIn</title>
</head>
<body>
Please wait...
<!-- Firebase -->
<!-- ***********************************************************************************************************************
* TODO(DEVELOPER): Paste the initialization snippet from: Firebase Console > Overview > Add Firebase to your web app. *
*********************************************************************************************************************** -->
<script>
/**
* Returns the value of the given URL query parameter.
*/
function getURLParameter(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) ||
[null, ''])[1].replace(/\+/g, '%20')) || null;
}
/**
* Returns the ID of the Firebase project.
*/
function getFirebaseProjectId() {
return firebase.app().options.authDomain.split('.')[0];
}
/**
* This callback is called by the JSONP callback of the 'token' Firebase Function with the Firebase auth token.
*/
function tokenReceived(data) {
if (data.token) {
firebase.auth().signInWithCustomToken(data.token).then(function() {
window.close();
}).catch(function(error) {
console.error(error);
document.body.innerText = 'Error While using the Firebase Auth token to sign in: ' + error.message;
})
} else {
console.error(error);
document.body.innerText = 'Error in the token Function: ' + data.error;
}
}
var code = getURLParameter('code');
var state = getURLParameter('state');
var error = getURLParameter('error');
if (!code) {
document.body.innerText = 'Error back from the LinkedIn auth page: ' + error;
} else {
// Use JSONP to load the 'token' Firebase Function to exchange the auth code against a Firebase custom token.
const script = document.createElement('script');
script.type = 'text/javascript';
// This is the URL to the HTTP triggered 'token' Firebase Function.
// See https://firebase.google.com/preview/functions/gcp-events#handle_a_cloud_http_firebase_function_event.
var tokenFunctionURL = 'https://us-central1-' + getFirebaseProjectId() + '.cloudfunctions.net/token';
script.src = tokenFunctionURL +
'?code=' + encodeURIComponent(code) +
'&state=' + encodeURIComponent(state) +
'&callback=' + tokenReceived.name;
document.head.appendChild(script);
}
</script>
</body>
</html>
Loading…
Cancel
Save