├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── index.js ├── App.test.js ├── index.css ├── App.css └── App.js ├── .gitignore ├── netlify.toml ├── functions ├── auth.js ├── utils │ ├── getUserData.js │ └── oauth.js └── auth-callback.js ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/intercom-netlify-oauth/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | } 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | font-family: Roboto,-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "FaunaDB Example", 3 | "name": "Fauna + Netlify Functions", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 100%; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | height: 100%; 7 | } 8 | 9 | .app-contents { 10 | padding-bottom: 150px; 11 | text-align: center; 12 | } 13 | 14 | .button-wrapper { 15 | margin-top: 25px; 16 | } 17 | 18 | .github-link { 19 | display: block; 20 | margin-top: 20px; 21 | text-decoration: none; 22 | color: #208ced; 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /functions-build 12 | 13 | # netlify 14 | .netlify 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # This will be run the site build 3 | command = "npm run build" 4 | # This is the directory is publishing to netlify's CDN 5 | publish = "build" 6 | # Location of built function code 7 | functions = "functions-build" 8 | 9 | [template.environment] 10 | # Environment variables needed 11 | INTERCOM_APP_ID = "Your Intercom App Id" 12 | INTERCOM_CLIENT_ID = "Your Intercom Oauth Client Id" 13 | INTERCOM_CLIENT_SECRET = "Your Intercom Oauth Secret" -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Intercom + Netlify Functions 12 | 13 | 14 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /functions/auth.js: -------------------------------------------------------------------------------- 1 | import oauth2, { config } from './utils/oauth' 2 | 3 | /* Do initial auth redirect */ 4 | exports.handler = (event, context, callback) => { 5 | /* Generate authorizationURI */ 6 | const authorizationURI = oauth2.authorizationCode.authorizeURL({ 7 | redirect_uri: config.redirect_uri, 8 | /* Specify how your app needs to access the user’s account. http://bit.ly/intercom-scopes */ 9 | scope: '', 10 | /* State helps mitigate CSRF attacks & Restore the previous state of your app */ 11 | state: '', 12 | }) 13 | 14 | /* Redirect user to authorizationURI */ 15 | const response = { 16 | statusCode: 302, 17 | headers: { 18 | Location: authorizationURI, 19 | 'Cache-Control': 'no-cache' // Disable caching of this response 20 | }, 21 | body: '' // return body for local dev 22 | } 23 | 24 | return callback(null, response) 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-intercom", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.4.0", 7 | "react-dom": "^16.4.0", 8 | "react-scripts": "1.1.4", 9 | "request": "^2.87.0", 10 | "simple-oauth2": "^2.2.0" 11 | }, 12 | "scripts": { 13 | "start": "npm-run-all --parallel start:**", 14 | "start:app": "react-scripts start", 15 | "start:server": "netlify-lambda serve functions", 16 | "build": "npm-run-all --parallel build:**", 17 | "build:app": "react-scripts build", 18 | "build:functions": "netlify-lambda build functions", 19 | "test": "react-scripts test --env=jsdom", 20 | "docs": "md-magic --path '**/*.md' --ignore 'node_modules'" 21 | }, 22 | "devDependencies": { 23 | "markdown-magic": "^0.1.23", 24 | "netlify-lambda": "^0.4.0", 25 | "npm-run-all": "^4.1.3" 26 | }, 27 | "proxy": { 28 | "/.netlify/functions": { 29 | "target": "http://localhost:9000", 30 | "pathRewrite": { 31 | "^/\\.netlify/functions": "" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './App.css' 3 | 4 | const imgBaseUrl = 'https://static.intercomassets.com/assets/oauth' 5 | 6 | export default class App extends Component { 7 | render() { 8 | return ( 9 |
10 |
11 |

Intercom + Netlify Functions

12 |

Login with Intercom OAuth

13 |
14 | 15 | Login with itercom 20 | 21 |
22 | 23 | 24 | View the source on Github 25 | 26 |
27 |
28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/utils/getUserData.js: -------------------------------------------------------------------------------- 1 | import request from 'request' 2 | import querystring from 'querystring' 3 | import { config } from './oauth' 4 | 5 | /* Call into https://app.intercom.io/me and return user data */ 6 | export default function getUserData(token) { 7 | const postData = querystring.stringify({ 8 | client_id: config.clientId, 9 | client_secret: config.clientSecret, 10 | app_id: config.appId 11 | }) 12 | 13 | const requestOptions = { 14 | url: `${config.profilePath}?${postData}`, 15 | json: true, 16 | auth: { 17 | user: token.token.token, 18 | pass: '', 19 | }, 20 | headers: { 21 | 'Content-Type': 'application/x-www-form-urlencoded', 22 | 'Accept': 'application/json', 23 | } 24 | } 25 | 26 | return requestWrapper(requestOptions, token) 27 | } 28 | 29 | /* promisify request call */ 30 | function requestWrapper(requestOptions, token) { 31 | return new Promise((resolve, reject) => { 32 | request(requestOptions, (err, response, body) => { 33 | if (err) { 34 | return reject(err) 35 | } 36 | // return data 37 | return resolve({ 38 | token: token, 39 | data: body, 40 | }) 41 | }) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /functions/utils/oauth.js: -------------------------------------------------------------------------------- 1 | import simpleOauth from 'simple-oauth2' 2 | 3 | const intercomApi = 'https://app.intercom.io' 4 | /* process.env.URL from netlify BUILD environment variables */ 5 | const siteUrl = process.env.URL || 'http://localhost:3000' 6 | 7 | export const config = { 8 | /* values set in terminal session or in netlify environment variables */ 9 | appId: process.env.INTERCOM_APP_ID, 10 | clientId: process.env.INTERCOM_CLIENT_ID, 11 | clientSecret: process.env.INTERCOM_CLIENT_SECRET, 12 | /* Intercom oauth API endpoints */ 13 | tokenHost: intercomApi, 14 | authorizePath: `${intercomApi}/oauth`, 15 | tokenPath: `${intercomApi}/auth/eagle/token`, 16 | profilePath: `${intercomApi}/me/`, 17 | /* redirect_uri is the callback url after successful signin */ 18 | redirect_uri: `${siteUrl}/.netlify/functions/auth-callback`, 19 | } 20 | 21 | function authInstance(credentials) { 22 | if (!credentials.client.id) { 23 | throw new Error('MISSING REQUIRED ENV VARS. Please set INTERCOM_CLIENT_ID') 24 | } 25 | if (!credentials.client.secret) { 26 | throw new Error('MISSING REQUIRED ENV VARS. Please set INTERCOM_CLIENT_SECRET') 27 | } 28 | // return oauth instance 29 | return simpleOauth.create(credentials) 30 | } 31 | 32 | /* Create oauth2 instance to use in our two functions */ 33 | export default authInstance({ 34 | client: { 35 | id: config.clientId, 36 | secret: config.clientSecret 37 | }, 38 | auth: { 39 | tokenHost: config.tokenHost, 40 | tokenPath: config.tokenPath, 41 | authorizePath: config.authorizePath 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /functions/auth-callback.js: -------------------------------------------------------------------------------- 1 | import getUserData from './utils/getUserData' 2 | import oauth2, { config } from './utils/oauth' 3 | 4 | /* Function to handle intercom auth callback */ 5 | exports.handler = (event, context, callback) => { 6 | const code = event.queryStringParameters.code 7 | /* state helps mitigate CSRF attacks & Restore the previous state of your app */ 8 | const state = event.queryStringParameters.state 9 | 10 | /* Take the grant code and exchange for an accessToken */ 11 | oauth2.authorizationCode.getToken({ 12 | code: code, 13 | redirect_uri: config.redirect_uri, 14 | client_id: config.clientId, 15 | client_secret: config.clientSecret 16 | }) 17 | .then((result) => { 18 | const token = oauth2.accessToken.create(result) 19 | console.log('accessToken', token) 20 | return token 21 | }) 22 | // Get more info about intercom user 23 | .then(getUserData) 24 | // Do stuff with user data & token 25 | .then((result) => { 26 | console.log('auth token', result.token) 27 | // Do stuff with user data 28 | console.log('user data', result.data) 29 | // Do other custom stuff 30 | console.log('state', state) 31 | // return results to browser 32 | return callback(null, { 33 | statusCode: 200, 34 | body: JSON.stringify(result) 35 | }) 36 | }) 37 | .catch((error) => { 38 | console.log('Access Token Error', error.message) 39 | console.log(error) 40 | return callback(null, { 41 | statusCode: error.statusCode || 500, 42 | body: JSON.stringify({ 43 | error: error.message, 44 | }) 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netlify + Intercom OAuth     2 | 3 | Add 'login with Intercom' via Netlify Functions & OAuth! 4 | 5 | 6 | - [About the project](#about-the-project) 7 | - [How to install and setup](#how-to-install-and-setup) 8 | - [Running the project locally](#running-the-project-locally) 9 | - [Deploying](#deploying) 10 | - [How it works with Netlify Functions](#how-it-works-with-netlify-functions) 11 | * [auth.js function](#authjs-function) 12 | * [auth-callback.js function](#auth-callbackjs-function) 13 | 14 | 15 | ## About the project 16 | 17 | This project sets up a "login with Intercom" OAuth flow using netlify functions. 18 | 19 | Here is a quick demo of the login flow, and the OAuth Access data you get back: 20 | 21 | ![Intercom oauth demo](https://user-images.githubusercontent.com/532272/42738995-7a8de2a0-8843-11e8-8179-d1865ded82ab.gif) 22 | 23 | You can leverage this project to wire up Intercom (or other OAuth providers) login with your application. 24 | 25 | > TLDR; [Watch the 11 minute video](https://www.youtube.com/watch?v=HuIS6jvK8S8) explaining **everything** 26 | 27 | 28 | 29 | --- 30 | 31 | Let's get started with how to get setup with the repo and with Intercom. 32 | 33 | ## How to install and setup 34 | 35 | 1. **Clone down the repository** 36 | 37 | ```bash 38 | git clone git@github.com:DavidWells/intercom-netlify-oauth.git 39 | ``` 40 | 41 | 2. **Install the dependencies** 42 | 43 | ```bash 44 | npm install 45 | ``` 46 | 47 | 3. **Create an Intercom OAuth app** 48 | 49 | Lets go ahead and setup the Intercom app we will need! 50 | 51 | [Create an Intercom OAuth app here](https://app.intercom.com/developers/) 52 | 53 | You need to enable a 'test' app in your account. It's a tricky to find but you can create a TEST app in your Intercom account under `Settings > General` 54 | 55 | `https://app.intercom.com/a/apps/your-app-id/settings/general` 56 | 57 | ![intercom-test-app-setup](https://user-images.githubusercontent.com/532272/42739711-0ec30506-8851-11e8-8c0a-b4b1d5bd4174.jpg) 58 | 59 | After enabling the test app, you can find it listed in your [intercom developer portal](https://app.intercom.com/developers/). 60 | 61 | We now need to configure the test app. 62 | 63 | Input the live "WEBSITE URL" and "REDIRECT URLS" in the app edit screen. 64 | 65 | ![itercom-oauth-app-settings](https://user-images.githubusercontent.com/532272/42740025-0ea5833c-8856-11e8-827a-369189b951a1.jpg) 66 | 67 | You will want to have your live Netlify site URL and `localhost:3000` setup to handle the redirects for local development. 68 | 69 | If you haven't deployed to Netlify yet, just insert a placeholder URL like `http://my-temp-site.com` but **remember to change this once your Netlify site is live with the correct URL** 70 | 71 | Our demo app has these `REDIRECT URLS` values that are comma separated 72 | 73 | ```bash 74 | https://intercom-login-example.netlify.com/.netlify/functions/auth-callback, 75 | http://localhost:3000/.netlify/functions/auth-callback 76 | ``` 77 | 78 | Great we are all configured over here. 79 | 80 | 4. **Grab your the required config values** 81 | 82 | We need our Intercom app values to configure our function environment variables. 83 | 84 | Navigate back to the main OAuth screen and grab the **App ID**, **Client ID**, and **Client Secret** values. We will need these to run the app locally and when deploying to Netlify. 85 | 86 | ![intercom-config-values](https://user-images.githubusercontent.com/532272/42739965-25d15c26-8855-11e8-925b-105c1fa381f5.jpg) 87 | 88 | ## Running the project locally 89 | 90 | Because we are using `netlify-lambda` to build & serve functions locally, we can work on this project without needing to redeploy to reflect changes! 91 | 92 | We need to set an Intercom app id and OAuth client id + secret in your terminal environment for the functions to connect to your Intercom app. 93 | 94 | After creating and configuring your [Intercom OAuth app](https://app.intercom.com/developers/), it's time to plugin the required environment variables into your local terminal session. 95 | 96 | On linux/MacOS, run the following command in your terminal: 97 | 98 | ```bash 99 | export INTERCOM_APP_ID=INTERCOM_APP_ID 100 | export INTERCOM_CLIENT_ID=INTERCOM_CLIENT_ID 101 | export INTERCOM_CLIENT_SECRET=INTERCOM_CLIENT_SECRET 102 | ``` 103 | 104 | If you are on a window machine, set the environment variable like so: 105 | 106 | ```bash 107 | set INTERCOM_APP_ID=INTERCOM_APP_ID 108 | set INTERCOM_CLIENT_ID=INTERCOM_CLIENT_ID 109 | set INTERCOM_CLIENT_SECRET=INTERCOM_CLIENT_SECRET 110 | ``` 111 | 112 | Then run the start command 113 | 114 | ```bash 115 | npm start 116 | ``` 117 | 118 | This will boot up our functions to run locally for development. You can now login via your Intercom application and see the token data returned. 119 | 120 | Making edits to the functions in the `/functions` will hot reload the server and you can iterate on building your custom logic. 121 | 122 | ## Deploying 123 | 124 | Use the one click "deploy to Netlify" button to launch this! 125 | 126 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/davidwells/intercom-netlify-oauth) 127 | 128 | Alternatively, you can connect this repo with your Netlify account and add in your values. 129 | 130 | In `https://app.netlify.com/sites/YOUR-SITE-SLUG/settings/deploys` add the `INTERCOM_APP_ID`, `INTERCOM_CLIENT_ID`, and `INTERCOM_CLIENT_SECRET` values to the "Build environment variables" section of settings 131 | 132 | ![intercom-deploy-settings](https://user-images.githubusercontent.com/532272/42740147-ece388c8-8857-11e8-93af-a1dd721e345a.jpg) 133 | 134 | After your site is deployed, you should be able to test your Intercom login flow. 135 | 136 | ## How it works with Netlify Functions 137 | 138 | Once again, serverless functions come to the rescue! 139 | 140 | We will be using 2 functions to handle the entire OAuth flow with Intercom. 141 | 142 | **Here is a diagram of what is happening:** 143 | 144 | ![Intercom oauth netlify](https://user-images.githubusercontent.com/532272/42144429-d2717f24-7d6f-11e8-8619-c1bec1562991.png) 145 | 146 | 1. First the `auth.js` function is triggered & redirects the user to Intercom 147 | 2. The user logs in via Intercom and is redirected back to `auth-callback.js` function with an **auth grant code** 148 | 3. `auth-callback.js` takes the **auth grant code** and calls back into Intercom's API to exchange it for an **AccessToken** 149 | 4. `auth-callback.js` now has the **AccessToken** to make any API calls it would like back into the Intercom App. 150 | 151 | This flow uses the [Authorization Code Grant](https://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1) flow. For more information on OAuth 2.0, [Watch this video](https://www.youtube.com/watch?v=CPbvxxslDTU) 152 | 153 | Let's dive into the individual functions and how they work. 154 | 155 | ### auth.js function 156 | 157 | The `auth.js` function creates an `authorizationURI` using the [`simple-oauth2` npm module](https://www.npmjs.com/package/simple-oauth2) and redirects the user to the Intercom login screen. 158 | 159 | Inside of the `auth.js` function, we set the `header.Location` in the lambda response and that will redirect the user to the `authorizationURI`, a.k.a the Intercom oauth login screen. 160 | 161 | 162 | 163 | ```js 164 | /* code from /functions/auth.js */ 165 | import oauth2, { config } from './utils/oauth' 166 | 167 | /* Do initial auth redirect */ 168 | exports.handler = (event, context, callback) => { 169 | /* Generate authorizationURI */ 170 | const authorizationURI = oauth2.authorizationCode.authorizeURL({ 171 | redirect_uri: config.redirect_uri, 172 | /* Specify how your app needs to access the user’s account. http://bit.ly/intercom-scopes */ 173 | scope: '', 174 | /* State helps mitigate CSRF attacks & Restore the previous state of your app */ 175 | state: '', 176 | }) 177 | 178 | /* Redirect user to authorizationURI */ 179 | const response = { 180 | statusCode: 302, 181 | headers: { 182 | Location: authorizationURI, 183 | 'Cache-Control': 'no-cache' // Disable caching of this response 184 | }, 185 | body: '' // return body for local dev 186 | } 187 | 188 | return callback(null, response) 189 | } 190 | ``` 191 | 192 | 193 | ### auth-callback.js function 194 | 195 | The `auth-callback.js` function handles the authorization grant code returned from the successful Intercom login. 196 | 197 | It then calls `oauth2.authorizationCode.getToken` to get a valid `accessToken` from Intercom. 198 | 199 | Once you have the valid accessToken, you can store it and make authenticated calls on behalf of the user to the Intercom API. 200 | 201 | 202 | 203 | ```js 204 | /* code from /functions/auth-callback.js */ 205 | import getUserData from './utils/getUserData' 206 | import oauth2, { config } from './utils/oauth' 207 | 208 | /* Function to handle intercom auth callback */ 209 | exports.handler = (event, context, callback) => { 210 | const code = event.queryStringParameters.code 211 | /* state helps mitigate CSRF attacks & Restore the previous state of your app */ 212 | const state = event.queryStringParameters.state 213 | 214 | /* Take the grant code and exchange for an accessToken */ 215 | oauth2.authorizationCode.getToken({ 216 | code: code, 217 | redirect_uri: config.redirect_uri, 218 | client_id: config.clientId, 219 | client_secret: config.clientSecret 220 | }) 221 | .then((result) => { 222 | const token = oauth2.accessToken.create(result) 223 | console.log('accessToken', token) 224 | return token 225 | }) 226 | // Get more info about intercom user 227 | .then(getUserData) 228 | // Do stuff with user data & token 229 | .then((result) => { 230 | console.log('auth token', result.token) 231 | // Do stuff with user data 232 | console.log('user data', result.data) 233 | // Do other custom stuff 234 | console.log('state', state) 235 | // return results to browser 236 | return callback(null, { 237 | statusCode: 200, 238 | body: JSON.stringify(result) 239 | }) 240 | }) 241 | .catch((error) => { 242 | console.log('Access Token Error', error.message) 243 | console.log(error) 244 | return callback(null, { 245 | statusCode: error.statusCode || 500, 246 | body: JSON.stringify({ 247 | error: error.message, 248 | }) 249 | }) 250 | }) 251 | } 252 | ``` 253 | 254 | 255 | Using two simple lambda functions, we can now handle logins via Intercom or any other third party OAuth provider. 256 | 257 | That's pretty nifty! 258 | --------------------------------------------------------------------------------