├── .gitignore ├── README.md ├── auth0_client.js ├── auth0_inline.js ├── auth0_server.js ├── end_of_inline_form_response.html ├── end_of_inline_form_response.js ├── iframe_inline_form.html ├── iframe_inline_form.js ├── oauth_inline_browser.js ├── oauth_inline_client.js ├── oauth_inline_server.js ├── package-lock.json ├── package.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .DS_Store 5 | 6 | # Dependency 7 | node_modules/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Meteor Auth0 OAuth 2.0 Provider 2 | =============================== 3 | 4 | This is our approach of integrating Auth0 into Meteor. It focuses on the implementation on the 5 | backend and is frontend-agnostic, so you can develop your own frontend or use Auth0 Lock (But I don't know how to do it the moment). 6 | 7 | 8 | ## Usage 9 | 10 | Set your Auth0 Domain, Client ID and Client Secret in `settings.json` (read more about how to store 11 | your API keys (Securing API Keys)[https://guide.meteor.com/security.html#api-keys] ): 12 | 13 | ``` 14 | "private": { 15 | "AUTH0_CLIENT_SECRET": YOUR_CLIENT_SECRET, 16 | /* ... other private keys */ 17 | }, 18 | "public": { 19 | "AUTH0_DOMAIN": yourauth0domain.eu.auth0.com 20 | "AUTH0_CLIENT_ID": YOUR_CLIENT_ID, 21 | /* ... other private keys */ 22 | } 23 | ``` 24 | 25 | ### Launch Login 26 | Then, you can simply initiate auth with on the client: 27 | ``` Meteor.loginWithAuth0() ``` 28 | 29 | ### Onescreener Branches 30 | ```master-stable``` - used for Live pages 31 | 32 | ```master-stable-onescreener-editor``` - used for Onescreener Editor 33 | 34 | 35 | ### Supported Options 36 | Value | Default | Description 37 | --- | --- | --- 38 | `loginStyle` | 'popup' | choose between *popup* (default) and *redirect* 39 | `type` | - | adds a hash to the url. Can be used to identify *login* and *signup* flows separately. 40 | `path` | - | redirect path after successful login 41 | 42 | ## Project Aim 43 | Although there are already [some other meteor-auth0 repositories out there](https://github.com/search?utf8=%E2%9C%93&q=meteor+auth0), this one has some different objectives: 44 | - Future ready: Use ES6 45 | - Separation of concerns: Auth0 can be used with or without Lock.js. This repo aims to be the common base. 46 | - Best practices: Use settings.json instead of Autopublish and databases. 47 | 48 | ## Thanks and further info 49 | - [Robfallow's Writing a Meteor OAuth 2 Handler](http://robfallows.github.io/2015/12/17/writing-an-oauth-2-handler.html) 50 | - [Auth0's Integrating a Web App with Auth0](https://auth0.com/docs/oauth-web-protocol) 51 | -------------------------------------------------------------------------------- /auth0_client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Meteor } from 'meteor/meteor' 4 | import { OAuth } from 'meteor/oauth' 5 | import { Accounts } from 'meteor/accounts-base' 6 | 7 | import { Auth0Inline } from './auth0_inline' 8 | 9 | const KEY_NAME = 'Meteor_Reload' 10 | const SIGNUP_AS = '/_signup' 11 | 12 | /** 13 | * Define the base object namespace. By convention we use the service name 14 | * in PascalCase (aka UpperCamelCase). Note that this is defined as a package global (boilerplate). 15 | */ 16 | 17 | Auth0 = {} 18 | 19 | Accounts.oauth.registerService('auth0') 20 | 21 | Meteor.loginWithAuth0 = function(options, callback) { 22 | /** 23 | * support (options, callback) and (callback) 24 | */ 25 | if (!callback && typeof options === 'function') { 26 | callback = options 27 | options = null 28 | } 29 | 30 | options.callback = callback 31 | 32 | /** 33 | * 34 | */ 35 | var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback) 36 | Auth0.requestCredential(options, credentialRequestCompleteCallback) 37 | } 38 | 39 | /** 40 | * Determine login style inclusive support for inline auth0 lock 41 | */ 42 | 43 | Auth0._loginStyle = function(config, options) { 44 | return ( 45 | (options.path === SIGNUP_AS && 'redirect') || 46 | (options.loginStyle === 'inline' && 'inline') || 47 | OAuth._loginStyle('auth0', config, options) 48 | ) 49 | } 50 | 51 | Auth0._rootUrl = function(options) { 52 | let rootUrl = Meteor.absoluteUrl('') 53 | 54 | if (options.rootUrl > '') { 55 | rootUrl = options.rootUrl.endsWith('/') ? options.rootUrl : `${options.rootUrl}/` 56 | } 57 | 58 | return rootUrl 59 | } 60 | 61 | /** 62 | * Request Auth0 credentials for the user (boilerplate). 63 | * Called from accounts-auth0. 64 | * 65 | * @param {Object} options Optional 66 | * @param {Function} credentialRequestCompleteCallback Callback function to call on completion. 67 | * Takes one argument, credentialToken on 68 | * success, or Error on error. 69 | */ 70 | 71 | Auth0.requestCredential = function(options, credentialRequestCompleteCallback) { 72 | /** 73 | * Support both (options, callback) and (callback). 74 | */ 75 | if (!credentialRequestCompleteCallback && typeof options === 'function') { 76 | credentialRequestCompleteCallback = options 77 | options = {} 78 | } else if (!options) { 79 | options = {} 80 | } 81 | 82 | /** 83 | * Make sure we have a config object for subsequent use (boilerplate) 84 | */ 85 | const config = { 86 | clientId: Meteor.settings.public.AUTH0_CLIENT_ID, 87 | hostname: 88 | (options.path === SIGNUP_AS && Meteor.settings.public.AUTH0_ORIGIN_DOMAIN) || 89 | Meteor.settings.public.AUTH0_DOMAIN, 90 | clientConfigurationBaseUrl: 91 | Meteor.settings.public.AUTH0_CLIENT_CONFIG_BASE_URL || 'https://cdn.eu.auth0.com/', 92 | loginStyle: 'redirect', 93 | } 94 | 95 | /** 96 | * Boilerplate 97 | */ 98 | 99 | // Create one-time credential secret token 100 | const credentialToken = Random.secret() 101 | 102 | // Detemines the login style 103 | const loginStyle = Auth0._loginStyle(config, options) 104 | const rootUrl = Auth0._rootUrl(options) 105 | const redirectUrl = `${rootUrl}${loginStyle === 'inline' ? '_oauth_inline' : '_oauth'}/auth0` 106 | 107 | // Determine path 108 | let path = options.path || '' 109 | path = path.startsWith('/') ? path.substring(1) : path 110 | const callbackUrl = `${rootUrl}${path}` 111 | 112 | /** 113 | * Imgur requires response_type and client_id 114 | * We use state to roundtrip a random token to help protect against CSRF (boilerplate) 115 | */ 116 | 117 | const state = OAuth._stateParam(loginStyle, credentialToken, callbackUrl) 118 | 119 | let loginUrl = 120 | `https://${config.hostname}/authorize/` + 121 | '?scope=openid%20profile%20email' + 122 | '&response_type=code' + 123 | '&client_id=' + 124 | config.clientId + 125 | '&state=' + 126 | state + 127 | `&redirect_uri=${redirectUrl}` 128 | 129 | if (options.type) { 130 | loginUrl = loginUrl + '#' + options.type 131 | } 132 | 133 | /** 134 | * Client initiates OAuth login request (boilerplate) 135 | */ 136 | OAuth.startLogin({ 137 | authenticatedCallback: options.authenticatedCallback, 138 | callbackUrl, 139 | clientConfigurationBaseUrl: config.clientConfigurationBaseUrl, 140 | credentialRequestCompleteCallback, 141 | credentialToken, 142 | lock: options.lock || {}, 143 | loginPath: path, 144 | loginService: 'auth0', 145 | loginStyle, 146 | loginType: options.type, 147 | loginUrl, 148 | popupOptions: config.popupOptions || { height: 600 }, 149 | redirectUrl, 150 | }) 151 | } 152 | 153 | OAuth.startLogin = options => { 154 | if (!options.loginService) throw new Error('login service required') 155 | 156 | if (options.loginStyle === 'inline') { 157 | Auth0Inline.showLock(options) 158 | } else { 159 | OAuth.launchLogin(options) 160 | } 161 | } 162 | 163 | // Get cookie if external login 164 | const getCookie = (name) => { 165 | // Split cookie string and get all individual name=value pairs in an array 166 | var cookieArr = document.cookie.split(';') 167 | 168 | // Loop through the array elements 169 | for (var i = 0; i < cookieArr.length; i++) { 170 | var cookiePair = cookieArr[i].split('=') 171 | 172 | /* Removing whitespace at the beginning of the cookie name 173 | and compare it with the given string */ 174 | if (name == cookiePair[0].trim()) { 175 | // Decode the cookie value and return 176 | return JSON.parse(decodeURIComponent(cookiePair[1])) 177 | } 178 | } 179 | 180 | // Return null if not found 181 | return null 182 | } 183 | 184 | const cookieMigrationData = getCookie(KEY_NAME) 185 | if (cookieMigrationData) { 186 | document.cookie = KEY_NAME + '=; max-age=0' 187 | } 188 | 189 | // Overwrite getDataAfterRedirect to attempt to get oauth login data from cookie if session storage is empty 190 | OAuth.getDataAfterRedirect = () => { 191 | let migrationData = Reload._migrationData('oauth') 192 | 193 | // Check for migration data in cookie 194 | if (!migrationData && cookieMigrationData) { 195 | migrationData = cookieMigrationData.oauth 196 | } 197 | 198 | if (!(migrationData && migrationData.credentialToken)) return null 199 | 200 | const { credentialToken } = migrationData 201 | const key = OAuth._storageTokenPrefix + credentialToken 202 | let credentialSecret 203 | 204 | try { 205 | credentialSecret = sessionStorage.getItem(key) 206 | sessionStorage.removeItem(key) 207 | } catch (e) { 208 | Meteor._debug('error retrieving credentialSecret', e) 209 | } 210 | 211 | return { 212 | loginService: migrationData.loginService, 213 | credentialToken, 214 | credentialSecret, 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /auth0_inline.js: -------------------------------------------------------------------------------- 1 | // Browser specific code for the OAuth package. 2 | import { Meteor } from 'meteor/meteor' 3 | import { Accounts } from 'meteor/accounts-base' 4 | import { OAuth } from 'meteor/oauth' 5 | 6 | export const Auth0Inline = { lock: undefined } 7 | 8 | const getOrigin = rootUrl => { 9 | return rootUrl.endsWith('/') ? rootUrl.substr(0, rootUrl.length - 1) : rootUrl 10 | } 11 | 12 | Auth0Inline.showLock = async options => { 13 | OAuth.saveDataForRedirect(options.loginService, options.credentialToken) 14 | 15 | const isLogin = options.loginType === 'login' 16 | const isSignup = options.loginType === 'signup' 17 | const nonce = Random.secret() 18 | const params = { 19 | state: OAuth._stateParam('inline', options.credentialToken, options.callbackUrl), 20 | scope: 'openid profile email', 21 | } 22 | 23 | const lockOptions = { 24 | configurationBaseUrl: options.clientConfigurationBaseUrl, 25 | auth: { 26 | redirect: false, 27 | responseType: 'token id_token', 28 | params, 29 | nonce, 30 | sso: true, 31 | }, 32 | allowedConnections: 33 | options.lock.connections || (isSignup && ['Username-Password-Authentication']) || null, 34 | rememberLastLogin: true, 35 | languageDictionary: options.lock.languageDictionary, 36 | theme: { 37 | logo: options.lock.logo, 38 | primaryColor: options.lock.primaryColor, 39 | }, 40 | avatar: null, 41 | closable: true, 42 | container: options.lock.containerId, 43 | allowLogin: isLogin, 44 | allowSignUp: isSignup, 45 | } 46 | 47 | // Close (destroy) previous lock instance 48 | Auth0Inline.closeLock(options) 49 | 50 | const { Auth0Lock } = await import('auth0-lock') 51 | 52 | // Create and configure new auth0 lock instance 53 | Auth0Inline.lock = new Auth0Lock( 54 | Meteor.settings.public.AUTH0_CLIENT_ID, 55 | Meteor.settings.public.AUTH0_DOMAIN, 56 | lockOptions 57 | ) 58 | 59 | // Authenticate the user in Meteor 60 | Auth0Inline.lock.on('authenticated', result => { 61 | Auth0Inline.onAuthenticated(result, options) 62 | }) 63 | 64 | // Check for active login session in Auth0 (silent autentication) 65 | Auth0Inline.lock.checkSession( 66 | { 67 | responseType: 'token id_token', 68 | nonce, 69 | }, 70 | (error, result) => { 71 | if (error) { 72 | // Show lock on error as user needs to sign in again 73 | Auth0Inline.lock.on('hide', () => { 74 | window.history.replaceState({}, document.title, '.') 75 | }) 76 | 77 | // Show lock 78 | Auth0Inline.lock.show() 79 | } else { 80 | // Authenticate the user in Meteor 81 | Auth0Inline.onAuthenticated(result, options) 82 | } 83 | } 84 | ) 85 | } 86 | 87 | Auth0Inline.onAuthenticated = (result, options) => { 88 | options.authenticatedCallback?.() 89 | 90 | // Get lock container element 91 | const lockContainer = document.getElementById(options.lock.containerId) 92 | let iFrame 93 | 94 | if (lockContainer) { 95 | /* 96 | * Add message event listener for auth0 response from iFrame 97 | */ 98 | 99 | window.addEventListener( 100 | 'message', 101 | event => { 102 | if (event.data.type === 'AUTH0_RESPONSE') { 103 | lockContainer.removeChild(iFrame) 104 | 105 | const origin = getOrigin(options.rootUrl || Meteor.absoluteUrl('')) 106 | 107 | if (event.origin === origin) { 108 | const { credentialSecret, credentialToken } = event.data 109 | 110 | Accounts.callLoginMethod({ 111 | methodArguments: [{ oauth: { credentialToken, credentialSecret } }], 112 | userCallback: options.callback && (err => options.callback(convertError(err))), 113 | }) 114 | } else { 115 | // Log missmatching origin 116 | } 117 | } 118 | }, 119 | false 120 | ) 121 | 122 | /* 123 | * Add iframe with autentication url for Meteor 124 | */ 125 | 126 | // Authenticate the user for the application 127 | const accessTokenQueryData = { 128 | access_token: result.accessToken, 129 | refresh_token: result.refreshToken, 130 | expires_in: result.expiresIn, 131 | state: result.state, 132 | type: 'token', 133 | } 134 | const accessTokenQuery = new URLSearchParams(accessTokenQueryData) 135 | 136 | const iFrameSourceUrl = options.redirectUrl + '?' + accessTokenQuery 137 | iFrame = document.createElement('iframe') 138 | iFrame.setAttribute('src', iFrameSourceUrl) 139 | iFrame.setAttribute('width', '0') 140 | iFrame.setAttribute('height', '0') 141 | lockContainer.appendChild(iFrame) 142 | 143 | // Remove login or signup hash from url 144 | window.history.replaceState({}, document.title, '.') 145 | } 146 | } 147 | 148 | Auth0Inline.closeLock = (options = {}) => { 149 | Auth0Inline.lock = undefined 150 | 151 | if (options.lock && options.lock.containerId > '') { 152 | // Get the container element 153 | const lockContainer = document.getElementById(options.lock.containerId) 154 | 155 | // As long as