├── .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
has a child node, remove it
156 | if (lockContainer && lockContainer.hasChildNodes()) {
157 | lockContainer.removeChild(lockContainer.firstChild)
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/auth0_server.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor'
2 | import { Accounts } from 'meteor/accounts-base'
3 | import { OAuth } from 'meteor/oauth'
4 |
5 | import { OAuthInline } from './oauth_inline_server'
6 |
7 | /**
8 | * Define the base object namespace. By convention we use the service name
9 | * in PascalCase (aka UpperCamelCase). Note that this is defined as a package global.
10 | */
11 |
12 | Auth0 = {}
13 |
14 | Auth0.whitelistedFields = ['id', 'email', 'picture', 'name']
15 |
16 | Accounts.oauth.registerService('auth0')
17 |
18 | Accounts.addAutopublishFields({
19 | forLoggedInUser: _.map(
20 | /**
21 | * Logged in user gets whitelisted fields + accessToken + expiresAt.
22 | */
23 | Auth0.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token
24 | function (subfield) {
25 | return 'services.auth0.' + subfield
26 | }
27 | ),
28 |
29 | forOtherUsers: _.map(
30 | /**
31 | * Other users get whitelisted fields without emails, because even with
32 | * autopublish, no legitimate web app should be publishing all users' emails.
33 | */
34 | _.without(Auth0.whitelistedFields, 'email', 'verified_email'),
35 | function (subfield) {
36 | return 'services.auth0.' + subfield
37 | }
38 | ),
39 | })
40 |
41 | // Insert a configuration-stub into the database. All the config should be configured
42 | // via settings.json
43 | Meteor.startup(() => {
44 | ServiceConfiguration.configurations.upsert(
45 | { service: 'auth0' },
46 | {
47 | $set: {
48 | _configViaSettings: true,
49 | },
50 | }
51 | )
52 | })
53 |
54 | const getToken = function (authResponse) {
55 | return {
56 | accessToken: authResponse.access_token,
57 | refreshToken: authResponse.refresh_token,
58 | expiresIn: authResponse.expires_in,
59 | username: authResponse.account_username,
60 | }
61 | }
62 |
63 | /**
64 | * Boilerplate hook for use by underlying Meteor code
65 | */
66 | Auth0.retrieveCredential = (credentialToken, credentialSecret) => {
67 | return OAuth.retrieveCredential(credentialToken, credentialSecret)
68 | }
69 |
70 | /**
71 | * Register this service with the underlying OAuth handler
72 | * (name, oauthVersion, urls, handleOauthRequest):
73 | * name = 'imgur'
74 | * oauthVersion = 2
75 | * urls = null for OAuth 2
76 | * handleOauthRequest = function(query) returns {serviceData, options} where options is optional
77 | * serviceData will end up in the user's services.imgur
78 | */
79 | OAuthInline.registerService('auth0', 2, null, function (query) {
80 | /**
81 | * Make sure we have a config object for subsequent use (boilerplate)
82 | */
83 | const config = {
84 | clientId: Meteor.settings.public.AUTH0_CLIENT_ID,
85 | secret: Meteor.settings.private.AUTH0_CLIENT_SECRET,
86 | hostname: Meteor.settings.public.AUTH0_DOMAIN,
87 | loginStyle: 'redirect',
88 | }
89 |
90 | /**
91 | * Get the token and username (Meteor handles the underlying authorization flow).
92 | * Note that the username comes from from this request in Imgur.
93 | */
94 | const response = query.type === 'token' ? getToken(query) : getTokens(config, query)
95 | const accessToken = response.accessToken
96 | const username = response.username
97 |
98 | /**
99 | * If we got here, we can now request data from the account endpoints
100 | * to complete our serviceData request.
101 | * The identity object will contain the username plus *all* properties
102 | * retrieved from the account and settings methods.
103 | */
104 | const identity = _.extend({ username }, getAccount(config, username, accessToken))
105 |
106 | /**
107 | * Build our serviceData object. This needs to contain
108 | * accessToken
109 | * expiresAt, as a ms epochtime
110 | * refreshToken, if there is one
111 | * id - note that there *must* be an id property for Meteor to work with
112 | * email
113 | * reputation
114 | * created
115 | * We'll put the username into the user's profile
116 | */
117 | const serviceData = {
118 | accessToken,
119 | expiresAt: +new Date() + 1000 * response.expiresIn,
120 | }
121 | if (response.refreshToken) {
122 | serviceData.refreshToken = response.refreshToken
123 | }
124 |
125 | _.extend(serviceData, identity)
126 |
127 | serviceData.id = identity.sub
128 |
129 | /**
130 | * Return the serviceData object along with an options object containing
131 | * the initial profile object with the username.
132 | */
133 | return {
134 | serviceData: serviceData,
135 | options: {
136 | profile: {
137 | name: response.username, // comes from the token request
138 | },
139 | },
140 | }
141 | })
142 |
143 | /**
144 | * The following three utility functions are called in the above code to get
145 | * the access_token, refresh_token and username (getTokens)
146 | * account data (getAccount)
147 | * settings data (getSettings)
148 | * repectively.
149 | */
150 |
151 | /** getTokens exchanges a code for a token in line with Imgur's documentation
152 | *
153 | * returns an object containing:
154 | * accessToken {String}
155 | * expiresIn {Integer} Lifetime of token in seconds
156 | * refreshToken {String} If this is the first authorization request
157 | * account_username {String} User name of the current user
158 | * token_type {String} Set to 'Bearer'
159 | *
160 | * @param {Object} config The OAuth configuration object
161 | * @param {Object} query The OAuth query object
162 | * @return {Object} The response from the token request (see above)
163 | */
164 |
165 | const getTokens = function (config, query) {
166 | const endpoint = `https://${config.hostname}/oauth/token`
167 | /**
168 | * Attempt the exchange of code for token
169 | */
170 | let response
171 | try {
172 | response = HTTP.post(endpoint, {
173 | headers: {
174 | Accept: 'application/json',
175 | 'User-Agent': `Meteor/${Meteor.release}`,
176 | },
177 | params: {
178 | code: query.code,
179 | client_id: config.clientId,
180 | client_secret: config.secret,
181 | grant_type: 'authorization_code',
182 | redirect_uri: OAuth._redirectUri('auth0', config),
183 | },
184 | })
185 | } catch (err) {
186 | throw _.extend(new Error(`Failed to complete OAuth handshake with Auth0. ${err.message}`), {
187 | response: err.response,
188 | })
189 | }
190 |
191 | if (response.data.error) {
192 | /**
193 | * The http response was a json object with an error attribute
194 | */
195 | throw new Error(`Failed to complete OAuth handshake with Auth0. ${response.data.error}`)
196 | } else {
197 | /** The exchange worked. We have an object containing
198 | * access_token
199 | * refresh_token
200 | * expires_in
201 | * token_type
202 | * account_username
203 | *
204 | * Return an appropriately constructed object
205 | */
206 | return getToken(response.data)
207 | }
208 | }
209 |
210 | /**
211 | * getAccount gets the basic Imgur account data
212 | *
213 | * returns an object containing:
214 | * id {Integer} The user's Imgur id
215 | * url {String} The account username as requested in the URI
216 | * bio {String} A basic description the user has filled out
217 | * reputation {Float} The reputation for the account.
218 | * created {Integer} The epoch time of account creation
219 | * pro_expiration {Integer/Boolean} False if not a pro user, their expiration date if they are.
220 | *
221 | * @param {Object} config The OAuth configuration object
222 | * @param {String} username The Imgur username
223 | * @param {String} accessToken The OAuth access token
224 | * @return {Object} The response from the account request (see above)
225 | */
226 | const getAccount = function (config, username, accessToken) {
227 | const endpoint = `https://${config.hostname}/userinfo`
228 | let accountObject
229 |
230 | /**
231 | * Note the strange .data.data - the HTTP.get returns the object in the response's data
232 | * property. Also, Imgur returns the data we want in a data property of the response data
233 | * Hence (response).data.data
234 | */
235 | try {
236 | accountObject = HTTP.get(endpoint, {
237 | headers: {
238 | Authorization: `Bearer ${accessToken}`,
239 | },
240 | })
241 |
242 | return accountObject.data
243 | } catch (err) {
244 | throw _.extend(new Error(`Failed to fetch account data from Auth0. ${err.message}`), {
245 | response: err.response,
246 | })
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/end_of_inline_form_response.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Login completed.
5 |
6 |
7 | ##CONFIG##
8 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/end_of_inline_form_response.js:
--------------------------------------------------------------------------------
1 | // NOTE: This file is added to the client as asset and hence ecmascript package has no effect here.
2 | (function () {
3 | var config = JSON.parse(document.getElementById('config').innerHTML)
4 |
5 | if (config.setCredentialToken && config.rootUrl) {
6 | var credentialToken = config.credentialToken
7 | var credentialSecret = config.credentialSecret
8 | var credentialString = JSON.stringify({
9 | credentialToken: credentialToken,
10 | credentialSecret: credentialSecret,
11 | })
12 |
13 | if (window.parent) {
14 | window.parent.postMessage(
15 | { type: 'AUTH0_RESPONSE', credentialToken, credentialSecret },
16 | config.rootUrl
17 | )
18 | } else {
19 | try {
20 | localStorage[config.storagePrefix + credentialToken] = credentialSecret
21 | } catch (err) {
22 | console.error(err)
23 | }
24 | }
25 | }
26 |
27 | if (!config.isCordova) {
28 | document.getElementById('completedText').style.display = 'block'
29 | }
30 | })()
31 |
--------------------------------------------------------------------------------
/iframe_inline_form.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
18 |
19 |
20 |
21 |
22 | ##CONFIG##
23 |
24 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/iframe_inline_form.js:
--------------------------------------------------------------------------------
1 | // NOTE: This file is added to the client as asset and hence ecmascript package has no effect here.
2 | Auth0Inline = {
3 | lock: undefined,
4 | }
5 |
6 | // function getStateParam(loginStyle, credentialToken) {
7 | // var state = {
8 | // loginStyle,
9 | // credentialToken,
10 | // }
11 |
12 | // // Encode base64 as not all login services URI-encode the state
13 | // // parameter when they pass it back to us.
14 | // // Use the 'base64' package here because 'btoa' isn't supported in IE8/9.
15 | // return Base64.encode(JSON.stringify(state))
16 | // }
17 |
18 | Auth0Inline.closeLock = function (containerId) {
19 | Auth0Inline.lock = undefined
20 |
21 | if (containerId > '') {
22 | // Get the container element
23 | var container = document.getElementById(containerId)
24 |
25 | // As long as has a child node, remove it
26 | if (container && container.hasChildNodes()) {
27 | container.removeChild(container.firstChild)
28 | }
29 | }
30 | }
31 |
32 | Auth0Inline.launchLock = function ({ containerId, config }) {
33 | // var { credentialToken, loginType, lock, redirectUrl, state, nonce, loginPath } = config
34 |
35 | if (config.credentialToken) {
36 | var isLogin = config.loginType === 'login'
37 | var isSignup = config.loginType === 'signup'
38 | var nonce = config.nonce
39 | var params = {
40 | state: config.state,
41 | scope: 'openid profile email',
42 | }
43 |
44 | // Set lock options
45 | var lockOptions = {
46 | configurationBaseUrl: config.settings.AUTH0_CLIENT_CONFIG_BASE_URL,
47 | auth: {
48 | redirectUrl: config.redirectUrl,
49 | params,
50 | nonce,
51 | sso: true,
52 | },
53 | allowedConnections:
54 | config.lock.connections || (isSignup && ['Username-Password-Authentication']) || null,
55 | rememberLastLogin: true,
56 | languageDictionary: config.lock.languageDictionary,
57 | theme: {
58 | logo: config.lock.logo,
59 | primaryColor: config.lock.primaryColor,
60 | },
61 | avatar: null,
62 | closable: true,
63 | container: containerId,
64 | allowLogin: isLogin,
65 | allowSignUp: isSignup,
66 | }
67 |
68 | // Close (destroy) previous lock instance
69 | Auth0Inline.closeLock(containerId)
70 |
71 | // Create and configure new auth0 lock instance
72 | Auth0Inline.lock = new Auth0Lock(
73 | config.settings.AUTH0_CLIENT_ID,
74 | config.settings.AUTH0_DOMAIN,
75 | lockOptions
76 | )
77 |
78 | // Check for active login session in Auth0 (silent autentication)
79 | Auth0Inline.lock.checkSession(
80 | {
81 | responseType: 'token id_token',
82 | nonce,
83 | },
84 | function (error, result) {
85 | if (error) {
86 | // Show lock on error as user needs to sign in again
87 | Auth0Inline.lock.on('hide', function () {
88 | window.history.replaceState({}, document.title, '.')
89 | })
90 |
91 | // Show lock
92 | Auth0Inline.lock.show()
93 | } else {
94 | // Authenticate the user for the application
95 | const accessTokenQueryData = {
96 | access_token: result.accessToken,
97 | refresh_token: result.refreshToken,
98 | expires_in: result.expiresIn,
99 | }
100 | const accessTokenQuery = new URLSearchParams(accessTokenQueryData)
101 | const loginUrl =
102 | config.redirectUrl + '?' + accessTokenQuery + '&type=token' + '&state=' + config.state
103 |
104 | window.location.href = loginUrl
105 | }
106 | }
107 | )
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/oauth_inline_browser.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 | const getOrigin = (rootUrl) => {
7 | return rootUrl.endsWith('/') ? rootUrl.substr(0, rootUrl.length - 1) : rootUrl
8 | }
9 |
10 | // Allow server to specify a specify subclass of errors. We should come
11 | // up with a more generic way to do this!
12 | const convertError = err => {
13 | if (err && err instanceof Meteor.Error &&
14 | err.error === Accounts.LoginCancelledError.numericError)
15 | return new Accounts.LoginCancelledError(err.reason);
16 | else
17 | return err;
18 | };
19 |
20 | // Adds an iframe to the container and shows the auth0 lock
21 | //
22 | // @param options
23 | // - lock: Options for lock,
24 | // - redirectUrl: Url to redirect the auth0 login to,
25 | // - loginType: Login type ('login', 'signup'),
26 | // - rootUrl: Application root url (normally retrieved from Meteor.absoluteUrl),
27 | // - state: State param for security check,
28 | export const showInlineLoginForm = (options) => {
29 | const loginElement = document.getElementById(options.lock.containerId)
30 | let iFrame
31 |
32 | if (loginElement) {
33 | /*
34 | * Add message event listener for auth0 response from iFrame
35 | */
36 |
37 | window.addEventListener(
38 | 'message',
39 | (event) => {
40 | if (event.data.type === 'AUTH0_RESPONSE') {
41 | loginElement.removeChild(iFrame)
42 |
43 | const origin = getOrigin(options.rootUrl || Meteor.absoluteUrl(''))
44 |
45 | if (event.origin === origin) {
46 | const { credentialSecret, credentialToken } = event.data
47 |
48 | Accounts.callLoginMethod({
49 | methodArguments: [{ oauth: { credentialToken, credentialSecret } }],
50 | userCallback: options.callback && ((err) => options.callback(convertError(err))),
51 | })
52 | } else {
53 | // Log missmatching origin
54 | }
55 | }
56 | },
57 | false
58 | )
59 |
60 | /*
61 | * Add iframe
62 | */
63 |
64 | const iFrameOptions = {
65 | credentialToken: options.credentialToken,
66 | lock: JSON.stringify(options.lock),
67 | loginType: options.loginType,
68 | state: options.state,
69 | }
70 | const iFrameQuery = Object.keys(iFrameOptions)
71 | .map((key) => `${key}=${encodeURIComponent(iFrameOptions[key])}`)
72 | .join('&')
73 | const iFrameSourceUrl = options.rootUrl + '_oauth_inline/auth0/form?' + iFrameQuery
74 | iFrame = document.createElement('iframe')
75 | iFrame.setAttribute('src', iFrameSourceUrl)
76 | iFrame.classList.add(options.lock.containerId + '__widget' )
77 | iFrame.setAttribute('width', '100%')
78 | iFrame.setAttribute('height', '100%')
79 | loginElement.appendChild(iFrame)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/oauth_inline_client.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor'
2 | import { OAuth } from 'meteor/oauth'
3 | import { Accounts } from 'meteor/accounts-base'
4 |
5 | import { showInlineLoginForm } from './oauth_inline_browser'
6 |
7 | const KEY_NAME = 'Meteor_Reload'
8 |
9 | export const OAuthInline = {
10 | showInlineLoginForm,
11 | }
12 |
13 | /*
14 | * Overwrite OAuth._loginStyle: Determine the login style (popup, inline or redirect as default) for this login flow.
15 | */
16 |
17 | OAuth._loginStyle = (service, config, options) => {
18 | if (Meteor.isCordova) {
19 | return 'popup'
20 | }
21 |
22 | let loginStyle = (options && options.loginStyle) || config.loginStyle || 'popup'
23 |
24 | if (!['popup', 'redirect', 'inline'].includes(loginStyle))
25 | throw new Error(`Invalid login style: ${loginStyle}`)
26 |
27 | // If we don't have session storage (for example, Safari in private
28 | // mode), the redirect login flow won't work, so fallback to the
29 | // popup style.
30 | if (loginStyle === 'redirect') {
31 | try {
32 | sessionStorage.setItem('Meteor.oauth.test', 'test')
33 | sessionStorage.removeItem('Meteor.oauth.test')
34 | } catch (e) {
35 | loginStyle = 'popup'
36 | }
37 | }
38 |
39 | return loginStyle
40 | }
41 |
42 |
43 | // Get cookie if external login
44 | function getCookie(name) {
45 | // Split cookie string and get all individual name=value pairs in an array
46 | var cookieArr = document.cookie.split(';')
47 |
48 | // Loop through the array elements
49 | for (var i = 0; i < cookieArr.length; i++) {
50 | var cookiePair = cookieArr[i].split('=')
51 |
52 | /* Removing whitespace at the beginning of the cookie name
53 | and compare it with the given string */
54 | if (name == cookiePair[0].trim()) {
55 | // Decode the cookie value and return
56 | return JSON.parse(decodeURIComponent(cookiePair[1]))
57 | }
58 | }
59 |
60 | // Return null if not found
61 | return null
62 | }
63 |
64 | const cookieMigrationData = getCookie(KEY_NAME)
65 | if (cookieMigrationData) {
66 | document.cookie = KEY_NAME + '=; max-age=0'
67 | }
68 |
69 | // Overwrite OAuth.getDataAfterRedirect: attempt to get oauth login data from cookie if session storage is empty
70 | OAuth.getDataAfterRedirect = () => {
71 | let migrationData = Reload._migrationData('oauth')
72 |
73 | // Check for migration data in cookie
74 | if (!migrationData && cookieMigrationData) {
75 | migrationData = cookieMigrationData.oauth
76 | }
77 |
78 | if (!(migrationData && migrationData.credentialToken)) return null
79 |
80 | const { credentialToken } = migrationData
81 | const key = OAuth._storageTokenPrefix + credentialToken
82 | let credentialSecret
83 |
84 | try {
85 | credentialSecret = sessionStorage.getItem(key)
86 | sessionStorage.removeItem(key)
87 | } catch (e) {
88 | Meteor._debug('error retrieving credentialSecret', e)
89 | }
90 |
91 | return {
92 | loginService: migrationData.loginService,
93 | credentialToken,
94 | credentialSecret,
95 | }
96 | }
--------------------------------------------------------------------------------
/oauth_inline_server.js:
--------------------------------------------------------------------------------
1 | import bodyParser from 'body-parser'
2 |
3 | import { OAuth } from 'meteor/oauth'
4 | import { Random } from 'meteor/random'
5 | import { RoutePolicy } from 'meteor/routepolicy'
6 | import { ServiceConfiguration } from 'meteor/service-configuration'
7 | import { WebApp } from 'meteor/webapp'
8 |
9 | RoutePolicy.declare('/_oauth_inline/', 'network')
10 |
11 | // Overwrite OAuth._loginStyleFromQuery: Check and determine login style. Valid options are 'popup', 'inline' or 'redirect'
12 | OAuth._loginStyleFromQuery = (query) => {
13 | let style
14 | // For backwards-compatibility for older clients, catch any errors
15 | // that result from parsing the state parameter. If we can't parse it,
16 | // set login style to popup by default.
17 | try {
18 | style = OAuth._stateFromQuery(query).loginStyle
19 | } catch (err) {
20 | style = 'popup'
21 | }
22 | if (!['popup', 'inline', 'redirect'].includes(style)) {
23 | throw new Error(`Unrecognized login style: ${style}`)
24 | }
25 | return style
26 | }
27 |
28 | export const OAuthInline = {}
29 |
30 | const registeredServices = {}
31 |
32 | // OAuthInline.registerService: Register a handler for an OAuth service. The handler will be called
33 | // when we get an incoming http request on /_oauth/{serviceName}. This
34 | // handler should use that information to fetch data about the user
35 | // logging in.
36 | //
37 | // @param name {String} e.g. "google", "facebook"
38 | // @param version {Number} OAuth version (1 or 2)
39 | // @param urls For OAuth1 only, specify the service's urls
40 | // @param handleOauthRequest {Function(oauthBinding|query)}
41 | // - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider
42 | // - (For OAuth2 only) query {Object} parameters passed in query string
43 | // - return value is:
44 | // - {serviceData:, (optional options:)} where serviceData should end
45 | // up in the user's services[name] field
46 | // - `null` if the user declined to give permissions
47 | //
48 | OAuthInline.registerService = (name, version, urls, handleOauthRequest) => {
49 | if (registeredServices[name]) throw new Error(`Already registered the ${name} OAuth service`)
50 |
51 | registeredServices[name] = {
52 | serviceName: name,
53 | version,
54 | urls,
55 | handleOauthRequest,
56 | }
57 |
58 | // Register service in underlying OAuth
59 | OAuth.registerService(name, version, urls, handleOauthRequest)
60 | }
61 |
62 | const middleware = (req, res, next) => {
63 | let requestData
64 | let requestType
65 |
66 | // Make sure to catch any exceptions because otherwise we'd crash
67 | // the runner
68 | try {
69 | const request = checkOauthRequest(req)
70 |
71 | if (!request?.serviceName) {
72 | // not an oauth request. pass to next middleware.
73 | next()
74 | return
75 | }
76 |
77 | const service = registeredServices[request.serviceName]
78 |
79 | // Skip everything if there's no service set by the oauth middleware
80 | if (!service) throw new Error(`Unexpected OAuth service ${request.serviceName}`)
81 |
82 | // Make sure we're configured
83 | ensureConfigured(request.serviceName)
84 |
85 | // Check for a registered handler
86 | const handler = OAuth._requestHandlers[service.version]
87 | if (!handler) throw new Error(`Unexpected OAuth version ${service.version}`)
88 |
89 | // Set request type and request data for response
90 |
91 | if (req.method === 'GET') {
92 | requestData = req.query
93 | } else {
94 | requestData = req.body
95 | }
96 |
97 | // Render response
98 | handler(service, requestData, res)
99 | } catch (requestError) {
100 | console.error('REQUEST ERROR', requestError)
101 | // if we got thrown an error, save it off, it will get passed to
102 | // the appropriate login call (if any) and reported there.
103 | //
104 | // The other option would be to display it in the popup tab that
105 | // is still open at this point, ignoring the 'close' or 'redirect'
106 | // we were passed. But then the developer wouldn't be able to
107 | // style the error or react to it in any way.
108 | if (requestData?.state && requestError instanceof Error) {
109 | try {
110 | // catch any exceptions to avoid crashing runner
111 | OAuth._storePendingCredential(OAuth._credentialTokenFromQuery(requestData), requestError)
112 | } catch (storeError) {
113 | // Ignore the error and just give up. If we failed to store the
114 | // error, then the login will just fail with a generic error.
115 | console.warn(
116 | 'Error in OAuth Server while storing pending login result.\n' + storeError.stack || storeError.message
117 | )
118 | }
119 |
120 | // Catch errors because any exception here will crash the runner.
121 | try {
122 | OAuthInline._endOfInlineFormResponse(res, {
123 | query: requestData,
124 | error: requestError,
125 | })
126 | } catch (responseError) {
127 | console.warn(
128 | 'Error generating end of login response\n' +
129 | (responseError.stack || responseError.message)
130 | )
131 | }
132 | }
133 | }
134 | }
135 |
136 | // Listen to incoming OAuth http requests
137 | WebApp.connectHandlers.use('/_oauth_inline', bodyParser.json())
138 | WebApp.connectHandlers.use('/_oauth_inline', bodyParser.urlencoded({ extended: false }))
139 | WebApp.connectHandlers.use(middleware)
140 |
141 | // Handle /_oauth_inline/* paths and extract the service name and request type.
142 | //
143 | // @returns {String|null} e.g. "auth0", or null if this isn't an oauth request
144 | const checkOauthRequest = (req) => {
145 | // req.url will be "/_oauth/" with an optional "?close".
146 | const i = req.url.indexOf('?')
147 | let barePath
148 | if (i === -1) barePath = req.url
149 | else barePath = req.url.substring(0, i)
150 | const splitPath = barePath.split('/')
151 |
152 | // Any non-oauth request will continue down the default
153 | // middlewares.
154 | if (splitPath[1] !== '_oauth_inline') return null
155 |
156 | // Find service based on url
157 | const serviceName = splitPath[2]
158 |
159 | if (serviceName !== 'auth0') return null
160 |
161 | // Define request type (login form or response token)
162 | return { serviceName }
163 | }
164 |
165 | // Make sure we're configured
166 | const ensureConfigured = (serviceName) => {
167 | if (!ServiceConfiguration.configurations.findOne({ service: serviceName })) {
168 | throw new ServiceConfiguration.ConfigError()
169 | }
170 | }
171 |
172 | const isSafe = (value) => {
173 | // This matches strings generated by `Random.secret` and
174 | // `Random.id`.
175 | return typeof value === 'string' && /^[a-zA-Z0-9\-_]+$/.test(value)
176 | }
177 |
178 | // Internal: used by the oauth1 and oauth2 packages
179 | const _renderOauthResults = OAuth._renderOauthResults
180 | OAuth._renderOauthResults = (res, query, credentialSecret) => {
181 | const details = {
182 | query,
183 | }
184 | if (query.error) {
185 | details.error = query.error
186 | } else {
187 | const token = OAuth._credentialTokenFromQuery(query)
188 | const secret = credentialSecret
189 | if (token && secret && isSafe(token) && isSafe(secret)) {
190 | details.credentials = { token: token, secret: secret }
191 | } else {
192 | details.error = 'invalid_credential_token_or_secret'
193 | }
194 | }
195 |
196 | const loginStyle = OAuth._loginStyleFromQuery(query)
197 |
198 | if (loginStyle === 'inline') {
199 | OAuthInline._endOfInlineFormResponse(res, details)
200 | } else {
201 | _renderOauthResults(res, query, credentialSecret)
202 | }
203 | }
204 |
205 | // This "template" (not a real Spacebars template, just an HTML file
206 | // with some ##PLACEHOLDER##s) communicates the credential secret back
207 | // to the main window and then closes the popup.
208 | OAuthInline._endOfInlineFormResponseTemplate = Assets.getText('end_of_inline_form_response.html')
209 |
210 | // It would be nice to use Blaze here, but it's a little tricky
211 | // because our mustaches would be inside a