session-sync-auth-site 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Setup
2
+
3
+ Run `node ./node_modules/session-sync-auth-site/src/createDBTables.js [mysql_connection_string] [user_table_name] [session_table_name]`
4
+
5
+ Example: `node ./node_modules/session-sync-auth-site/src/createDBTables.js mysql://root@localhost/SessionSyncAuthSite users sessions`
6
+
7
+ You may add on more fields to the user and session tables, if you like.
8
+
9
+ # Simple backend usage
10
+
11
+ ```js
12
+ const express = require('express')
13
+ const app = express()
14
+ const cors = require('cors')
15
+ const bodyParser = require('body-parser')
16
+
17
+ const { authenticate, setUpSessionSyncAuthRoutes } = require('session-sync-auth-site')
18
+
19
+ app.use(cors())
20
+ app.use(bodyParser.json())
21
+
22
+ app.use(authenticate({
23
+ // either `connectionObj` or `connectionStr` is required
24
+ connectionObj: {
25
+ host,
26
+ user,
27
+ password,
28
+ database,
29
+ port,
30
+ },
31
+ }))
32
+
33
+ setUpSessionSyncAuthRoutes({
34
+ app,
35
+ siteId,
36
+ authDomain,
37
+ jwtSecret,
38
+ })
39
+ ```
40
+
41
+ ## Exhaustive options for authenticate with default values
42
+
43
+ ```js
44
+ app.use(authenticate({
45
+ // either `connectionObj` or `connectionStr` required
46
+ connectionObj: {
47
+ host,
48
+ user,
49
+ password,
50
+ database,
51
+ port,
52
+ },
53
+ userTableName: 'users',
54
+ sessionTableName: 'sessions',
55
+ userTableColNameMap: {
56
+ // Example:
57
+ // updated_at: 'updatedAt',
58
+ },
59
+ sessionTableColNameMap: {},
60
+ }))
61
+ ```
62
+
63
+ ## Exhaustive options for setUpSessionSyncAuthRoutes with default values
64
+
65
+ ```js
66
+ setUpSessionSyncAuthRoutes({
67
+ app, // required
68
+ siteId, // required
69
+ authDomain, // required
70
+ jwtSecret, // required
71
+ protocol: 'https',
72
+ paths: {
73
+ getUser: '/get-user',
74
+ logIn: '/log-in',
75
+ logOut: '/log-out',
76
+ authSync: '/auth-sync',
77
+ },
78
+ languageColType: '639-3', // OPTIONS: '639-1', '639-3', 'IETF'
79
+ })
80
+ ```
81
+
82
+ # Frontend usage
83
+
84
+ ```html
85
+ <html>
86
+ <head>
87
+ <script src="[private_url]/sessionSyncAuthFrontend.js"></script>
88
+
89
+ <script>
90
+
91
+ window.sessionSyncAuth.init({
92
+ defaultOrigin: 'https://my-backend-domain.com',
93
+ callbacks: {
94
+ canceledLogin: ({ origin }) => {},
95
+ successfulLogin: ({ origin, accessToken }) => {},
96
+ successfulLogout: ({ origin }) => {},
97
+ unnecessaryLogout: ({ origin }) => {},
98
+ error: ({ errorMessage }) => {},
99
+ },
100
+ })
101
+
102
+ // To change the default origin...
103
+ // window.sessionSyncAuth.setDefaultOrigin('https://my-backend-domain.com')
104
+
105
+ </script>
106
+ <head>
107
+
108
+ <body>
109
+
110
+ <!-- All functions below can also take a single options parameter with an `origin` key. -->
111
+
112
+ <button onclick="javascript:window.sessionSyncAuth.getAccessToken()">Get Access Token</button>
113
+
114
+ <button onclick="javascript:window.sessionSyncAuth.logIn()">Log in</button>
115
+
116
+ <button onclick="javascript:window.sessionSyncAuth.getUser()">Get user</button>
117
+
118
+ <button onclick="javascript:window.sessionSyncAuth.logOut()">Log out</button>
119
+
120
+ </body>
121
+
122
+ </html>
123
+ ```
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "session-sync-auth-site",
3
+ "version": "0.2.0",
4
+ "main": "src/index.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/educational-resources-and-services/session-sync-auth-site.git"
8
+ },
9
+ "author": "Andy Hubert",
10
+ "license": "MIT",
11
+ "bugs": {
12
+ "url": "https://github.com/educational-resources-and-services/session-sync-auth-site/issues"
13
+ },
14
+ "homepage": "https://github.com/educational-resources-and-services/session-sync-auth-site#readme",
15
+ "engines": {
16
+ "node": ">=10"
17
+ },
18
+ "scripts": {
19
+ "dev": "npm run go-dev -s",
20
+ "go-dev": "concurrently -k 'node ./test/dummySite.js 3001' 'node ./test/dummySite.js 3002 dummy1' 'node ./test/dummySite.js 3003 dummy2'",
21
+ "dev-with-staging-auth": "npm run go-dev-with-staging-auth -s",
22
+ "go-dev-with-staging-auth": "concurrently -k 'node ./test/dummySite.js 3001' 'node ./test/dummySite.js 3002 dummy1 auth.staging.resourcingeducation.com' 'node ./test/dummySite.js 3003 dummy2 auth.staging.resourcingeducation.com'",
23
+ "dev-with-production-auth": "npm run go-dev-with-production-auth -s",
24
+ "go-dev-with-production-auth": "concurrently -k 'node ./test/dummySite.js 3001' 'node ./test/dummySite.js 3002 dummy1 auth.resourcingeducation.com' 'node ./test/dummySite.js 3003 dummy2 auth.resourcingeducation.com'",
25
+ "setup": "npm run go-setup -s",
26
+ "go-setup": "npm run go-setup-dummy1 && npm run go-setup-dummy2",
27
+ "go-setup-dummy1": "node ./src/createDBTables.js mysql://root@localhost/SessionSyncAuthSite dummy1_users dummy1_sessions",
28
+ "go-setup-dummy2": "node ./src/createDBTables.js mysql://root@localhost/SessionSyncAuthSite dummy2_users dummy2_sessions",
29
+ "confirm": "read -p 'Are you sure? ' -n 1 -r && echo '\n' && [[ $REPLY =~ ^[Yy]$ ]]",
30
+ "update-patch": "npm run go-update-patch -s",
31
+ "update-major": "npm run go-update-major -s",
32
+ "update-minor": "npm run go-update-minor -s",
33
+ "go-update-patch": "echo '-------------------------------------------\nUpdate version (PATCH) and deploy to npm...\n-------------------------------------------\n' && npm run confirm && npm i && npm version patch && npm run publish-to-npm",
34
+ "go-update-minor": "echo '-------------------------------------------\nUpdate version (MINOR) and deploy to npm...\n-------------------------------------------\n' && npm run confirm && npm i && npm version minor && npm run publish-to-npm",
35
+ "go-update-major": "echo '-------------------------------------------\nUpdate version (MAJOR) and deploy to npm...\n-------------------------------------------\n' && npm run confirm && npm i && npm version major && npm run publish-to-npm",
36
+ "publish-to-npm": "npm publish --access public && echo '\nSUCCESS!\n'"
37
+ },
38
+ "peerDependencies": {
39
+ "express": ">= 4"
40
+ },
41
+ "devDependencies": {
42
+ "body-parser": "^1.19.0",
43
+ "concurrently": "^6.0.2",
44
+ "cors": "^2.8.5",
45
+ "express": "^4.17.1"
46
+ },
47
+ "dependencies": {
48
+ "connection-string": "^4.3.2",
49
+ "jsonwebtoken": "^8.5.1",
50
+ "mysql": "^2.18.1"
51
+ }
52
+ }
@@ -0,0 +1,75 @@
1
+ const setUpConnection = require('./setUpConnection')
2
+
3
+ const authenticate = ({
4
+ userTableName='users',
5
+ sessionTableName='sessions',
6
+ userTableColNameMap={},
7
+ sessionTableColNameMap={},
8
+ ...connectionInfo // connectionObj or connectionStr
9
+ }) => (
10
+ async (req, res, next) => {
11
+
12
+ userTableName = userTableName.replace(/`/g, '')
13
+ sessionTableName = sessionTableName.replace(/`/g, '')
14
+
15
+ req.sessionSyncAuthSiteOptions = {
16
+ userTableName,
17
+ sessionTableName,
18
+ userTableColNameMap,
19
+ sessionTableColNameMap,
20
+ }
21
+
22
+ // Connect to DB if not already connected
23
+ if(!global.sessionSyncAuthSiteConnection) {
24
+ console.log('Establishing DB connection for session-sync-auth-site...')
25
+ setUpConnection(connectionInfo)
26
+ console.log('...DB connection established for session-sync-auth-site.')
27
+ }
28
+
29
+ const accessToken = req.headers['x-access-token'] || req.query.accessToken
30
+
31
+ if(!accessToken) return next()
32
+
33
+ const userTableId = (userTableColNameMap.id || 'id').replace(/`/g, '')
34
+ const sessionTableCreatedAt = (sessionTableColNameMap.created_at || 'created_at').replace(/`/g, '')
35
+ const sessionTableUserId = (sessionTableColNameMap.user_id || 'user_id').replace(/`/g, '')
36
+ const sessionTableAccessToken = (sessionTableColNameMap.access_token || 'access_token').replace(/`/g, '')
37
+
38
+ req.user = (await global.sessionSyncAuthSiteConnection.asyncQuery(
39
+ `
40
+
41
+ SELECT
42
+ u.*,
43
+ s.\`${sessionTableCreatedAt}\` AS session_created_at
44
+
45
+ FROM \`${userTableName}\` AS u
46
+ LEFT JOIN \`${sessionTableName}\` AS s ON (u.\`${userTableId}\` = s.\`${sessionTableUserId}\`)
47
+
48
+ WHERE
49
+ s.\`${sessionTableAccessToken}\` = :accessToken
50
+
51
+ LIMIT 1
52
+
53
+ `,
54
+ {
55
+ accessToken,
56
+ },
57
+ ))[0]
58
+
59
+ // Convert DateTime columns into ms timestamp
60
+ const dateTimeCols = [
61
+ userTableColNameMap.created_at || 'created_at',
62
+ userTableColNameMap.updated_at || 'updated_at',
63
+ 'session_created_at',
64
+ ]
65
+ Object.keys(req.user || {}).forEach(key => {
66
+ if(dateTimeCols.includes(key)) {
67
+ req.user[key] = (new Date(req.user[key])).getTime()
68
+ }
69
+ })
70
+
71
+ next()
72
+ }
73
+ )
74
+
75
+ module.exports = authenticate
@@ -0,0 +1,97 @@
1
+ const setUpConnection = require('./setUpConnection')
2
+
3
+ ;(async () => {
4
+
5
+ try {
6
+
7
+ let [
8
+ connectionStr,
9
+ userTableName=`users`,
10
+ sessionTableName=`sessions`,
11
+ x,
12
+ ] = process.argv.slice(2)
13
+
14
+ userTableName = userTableName.replace(/`/g, '')
15
+ sessionTableName = sessionTableName.replace(/`/g, '')
16
+
17
+ if(connectionStr === undefined) {
18
+ throw new Error(`NO_PARAMS`)
19
+ }
20
+
21
+ if(userTableName === undefined || sessionTableName === undefined || x !== undefined) {
22
+ throw new Error(`BAD_PARAMS`)
23
+ }
24
+
25
+ setUpConnection({ connectionStr })
26
+
27
+ await global.sessionSyncAuthSiteConnection.asyncQuery(
28
+ `
29
+
30
+ CREATE TABLE \`${userTableName}\` (
31
+ \`id\` int NOT NULL,
32
+ \`name\` varchar(255),
33
+ \`email\` varchar(255),
34
+ \`image\` varchar(255),
35
+ \`language\` varchar(25),
36
+ \`created_at\` datetime(3) NOT NULL,
37
+ \`updated_at\` datetime(3) NOT NULL,
38
+ PRIMARY KEY (\`id\`),
39
+ UNIQUE KEY \`email\` (\`email\`)
40
+ ) CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
41
+
42
+ CREATE TABLE \`${sessionTableName}\` (
43
+ \`id\` int NOT NULL AUTO_INCREMENT,
44
+ \`user_id\` int NOT NULL,
45
+ \`access_token\` varchar(255) NOT NULL,
46
+ \`created_at\` datetime(3) NOT NULL,
47
+ PRIMARY KEY (\`id\`),
48
+ UNIQUE KEY \`access_token\` (\`access_token\`)
49
+ ) CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
50
+
51
+ `,
52
+ {},
53
+ )
54
+
55
+ console.log(``)
56
+ console.log(`Tables ${userTableName} and ${sessionTableName} created.`)
57
+ console.log(``)
58
+ console.log(`Note: The user table may be extended with other columns so long as they are nullable.`)
59
+ console.log(``)
60
+
61
+ } catch(err) {
62
+
63
+ const logSyntax = () => {
64
+ console.log(`Syntax: \`npm run create-db-tables connectionStr [userTableName] [sessionTableName]\`\n`)
65
+ console.log(`Example #1: \`npm run create-db-tables mysql://user:pass@host/db\``)
66
+ console.log(`Example #2: \`npm run create-db-tables mysql://user:pass@host/db users sessions\``)
67
+ console.log(``)
68
+ }
69
+
70
+ switch(err.message.split(',')[0]) {
71
+
72
+ case `NO_PARAMS`: {
73
+ logSyntax()
74
+ break
75
+ }
76
+
77
+ case `BAD_PARAMS`: {
78
+ console.log(`\n-----------------------`)
79
+ console.log(`ERROR: Bad parameters.`)
80
+ console.log(`-----------------------\n`)
81
+ logSyntax()
82
+ break
83
+ }
84
+
85
+ default: {
86
+ console.log(`\n-----------------------`)
87
+ console.log(`\nERROR: ${err.message}\n`)
88
+ console.log(`-----------------------\n`)
89
+ logSyntax()
90
+ }
91
+
92
+ }
93
+ }
94
+
95
+ process.exit()
96
+
97
+ })()
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ const authenticate = require('./authenticate')
2
+ const setUpSessionSyncAuthRoutes = require('./setUpSessionSyncAuthRoutes')
3
+
4
+ module.exports = {
5
+ authenticate,
6
+ setUpSessionSyncAuthRoutes,
7
+ }
@@ -0,0 +1,190 @@
1
+ // Converted from https://github.com/wooorm/iso-639-3/blob/main/iso6393-to-1.js
2
+
3
+ const iso6393To1 = {
4
+ aar: 'aa',
5
+ abk: 'ab',
6
+ afr: 'af',
7
+ aka: 'ak',
8
+ amh: 'am',
9
+ ara: 'ar',
10
+ arg: 'an',
11
+ asm: 'as',
12
+ ava: 'av',
13
+ ave: 'ae',
14
+ aym: 'ay',
15
+ aze: 'az',
16
+ bak: 'ba',
17
+ bam: 'bm',
18
+ bel: 'be',
19
+ ben: 'bn',
20
+ bis: 'bi',
21
+ bod: 'bo',
22
+ bos: 'bs',
23
+ bre: 'br',
24
+ bul: 'bg',
25
+ cat: 'ca',
26
+ ces: 'cs',
27
+ cha: 'ch',
28
+ che: 'ce',
29
+ chu: 'cu',
30
+ chv: 'cv',
31
+ cor: 'kw',
32
+ cos: 'co',
33
+ cre: 'cr',
34
+ cym: 'cy',
35
+ dan: 'da',
36
+ deu: 'de',
37
+ div: 'dv',
38
+ dzo: 'dz',
39
+ ell: 'el',
40
+ eng: 'en',
41
+ epo: 'eo',
42
+ est: 'et',
43
+ eus: 'eu',
44
+ ewe: 'ee',
45
+ fao: 'fo',
46
+ fas: 'fa',
47
+ fij: 'fj',
48
+ fin: 'fi',
49
+ fra: 'fr',
50
+ fry: 'fy',
51
+ ful: 'ff',
52
+ gla: 'gd',
53
+ gle: 'ga',
54
+ glg: 'gl',
55
+ glv: 'gv',
56
+ grn: 'gn',
57
+ guj: 'gu',
58
+ hat: 'ht',
59
+ hau: 'ha',
60
+ hbs: 'sh',
61
+ heb: 'he',
62
+ her: 'hz',
63
+ hin: 'hi',
64
+ hmo: 'ho',
65
+ hrv: 'hr',
66
+ hun: 'hu',
67
+ hye: 'hy',
68
+ ibo: 'ig',
69
+ ido: 'io',
70
+ iii: 'ii',
71
+ iku: 'iu',
72
+ ile: 'ie',
73
+ ina: 'ia',
74
+ ind: 'id',
75
+ ipk: 'ik',
76
+ isl: 'is',
77
+ ita: 'it',
78
+ jav: 'jv',
79
+ jpn: 'ja',
80
+ kal: 'kl',
81
+ kan: 'kn',
82
+ kas: 'ks',
83
+ kat: 'ka',
84
+ kau: 'kr',
85
+ kaz: 'kk',
86
+ khm: 'km',
87
+ kik: 'ki',
88
+ kin: 'rw',
89
+ kir: 'ky',
90
+ kom: 'kv',
91
+ kon: 'kg',
92
+ kor: 'ko',
93
+ kua: 'kj',
94
+ kur: 'ku',
95
+ lao: 'lo',
96
+ lat: 'la',
97
+ lav: 'lv',
98
+ lim: 'li',
99
+ lin: 'ln',
100
+ lit: 'lt',
101
+ ltz: 'lb',
102
+ lub: 'lu',
103
+ lug: 'lg',
104
+ mah: 'mh',
105
+ mal: 'ml',
106
+ mar: 'mr',
107
+ mkd: 'mk',
108
+ mlg: 'mg',
109
+ mlt: 'mt',
110
+ mon: 'mn',
111
+ mri: 'mi',
112
+ msa: 'ms',
113
+ mya: 'my',
114
+ nau: 'na',
115
+ nav: 'nv',
116
+ nbl: 'nr',
117
+ nde: 'nd',
118
+ ndo: 'ng',
119
+ nep: 'ne',
120
+ nld: 'nl',
121
+ nno: 'nn',
122
+ nob: 'nb',
123
+ nor: 'no',
124
+ nya: 'ny',
125
+ oci: 'oc',
126
+ oji: 'oj',
127
+ ori: 'or',
128
+ orm: 'om',
129
+ oss: 'os',
130
+ pan: 'pa',
131
+ pli: 'pi',
132
+ pol: 'pl',
133
+ por: 'pt',
134
+ pus: 'ps',
135
+ que: 'qu',
136
+ roh: 'rm',
137
+ ron: 'ro',
138
+ run: 'rn',
139
+ rus: 'ru',
140
+ sag: 'sg',
141
+ san: 'sa',
142
+ sin: 'si',
143
+ slk: 'sk',
144
+ slv: 'sl',
145
+ sme: 'se',
146
+ smo: 'sm',
147
+ sna: 'sn',
148
+ snd: 'sd',
149
+ som: 'so',
150
+ sot: 'st',
151
+ spa: 'es',
152
+ sqi: 'sq',
153
+ srd: 'sc',
154
+ srp: 'sr',
155
+ ssw: 'ss',
156
+ sun: 'su',
157
+ swa: 'sw',
158
+ swe: 'sv',
159
+ tah: 'ty',
160
+ tam: 'ta',
161
+ tat: 'tt',
162
+ tel: 'te',
163
+ tgk: 'tg',
164
+ tgl: 'tl',
165
+ tha: 'th',
166
+ tir: 'ti',
167
+ ton: 'to',
168
+ tsn: 'tn',
169
+ tso: 'ts',
170
+ tuk: 'tk',
171
+ tur: 'tr',
172
+ twi: 'tw',
173
+ uig: 'ug',
174
+ ukr: 'uk',
175
+ urd: 'ur',
176
+ uzb: 'uz',
177
+ ven: 've',
178
+ vie: 'vi',
179
+ vol: 'vo',
180
+ wln: 'wa',
181
+ wol: 'wo',
182
+ xho: 'xh',
183
+ yid: 'yi',
184
+ yor: 'yo',
185
+ zha: 'za',
186
+ zho: 'zh',
187
+ zul: 'zu'
188
+ }
189
+
190
+ module.exports = iso6393To1
@@ -0,0 +1,102 @@
1
+ ;(() => {
2
+
3
+ let defaultOrigin
4
+ const setDefaultOrigin = origin => {
5
+ defaultOrigin = origin
6
+ }
7
+
8
+ const getAccessToken = ({ origin=defaultOrigin }={}) => (
9
+ JSON.parse(localStorage.getItem('sessionSyncAuthAccessTokenByOrigin') || `{}`)[origin]
10
+ )
11
+
12
+ const getQueryStringAddOn = extraQueryParamsForCallbacks => {
13
+ let queryStringAddOn = ''
14
+
15
+ for(let key in extraQueryParamsForCallbacks) {
16
+ queryStringAddOn += `&${encodeURIComponent(key)}=${encodeURIComponent(extraQueryParamsForCallbacks[key])}`
17
+ }
18
+
19
+ return queryStringAddOn
20
+ }
21
+
22
+ const logIn = async ({ origin=defaultOrigin, extraQueryParamsForCallbacks }={}) => {
23
+ const cancelRedirectUrl = `${location.href.replace(/\?.*$/, '')}?action=canceledLogin&origin=${encodeURIComponent(origin)}${getQueryStringAddOn(extraQueryParamsForCallbacks)}`
24
+ const loggedInRedirectUrl = `${location.href.replace(/\?.*$/, '')}?action=successfulLogin&origin=${encodeURIComponent(origin)}&accessToken=ACCESS_TOKEN${getQueryStringAddOn(extraQueryParamsForCallbacks)}`
25
+
26
+ const queryString = `cancelRedirectUrl=${encodeURIComponent(cancelRedirectUrl)}&loggedInRedirectUrl=${encodeURIComponent(loggedInRedirectUrl)}`
27
+
28
+ window.location = `${origin}/log-in?${queryString}`
29
+ }
30
+
31
+ const getUser = async ({ origin=defaultOrigin }={}) => {
32
+ const response = await fetch(`${origin}/get-user`, {
33
+ headers: {
34
+ 'x-access-token': getAccessToken(origin),
35
+ },
36
+ })
37
+
38
+ return await response.json()
39
+ }
40
+
41
+ const logOut = async ({ origin=defaultOrigin, extraQueryParamsForCallbacks }={}) => {
42
+ const redirectUrl = `${location.href.replace(/\?.*$/, '')}?action=successfulLogout&origin=${encodeURIComponent(origin)}${getQueryStringAddOn(extraQueryParamsForCallbacks)}`
43
+ const noLoginRedirectUrl = `${location.href.replace(/\?.*$/, '')}?action=unnecessaryLogout&origin=${encodeURIComponent(origin)}${getQueryStringAddOn(extraQueryParamsForCallbacks)}`
44
+ const accessToken = getAccessToken(origin)
45
+
46
+ const queryString = `redirectUrl=${encodeURIComponent(redirectUrl)}&noLoginRedirectUrl=${encodeURIComponent(noLoginRedirectUrl)}&accessToken=${encodeURIComponent(accessToken)}`
47
+
48
+ window.location = `${origin}/log-out?${queryString}`
49
+ }
50
+
51
+ const init = ({ defaultOrigin, callbacks }) => {
52
+
53
+ if(defaultOrigin) setDefaultOrigin(defaultOrigin)
54
+
55
+ const getParam = regex => decodeURIComponent((window.location.search.match(regex) || [])[1] || '') || null
56
+
57
+ const origin = getParam(/[?&]origin=([^&]*)/)
58
+ const accessToken = getParam(/[?&]accessToken=([^&]*)/)
59
+ const action = getParam(/[?&]action=([^&]*)/)
60
+ const errorMessage = getParam(/[?&]errorMessage=([^&]*)/)
61
+
62
+ if(origin) {
63
+ const sessionSyncAuthAccessTokenByOrigin = JSON.parse(localStorage.getItem('sessionSyncAuthAccessTokenByOrigin') || `{}`)
64
+ if(accessToken) {
65
+ sessionSyncAuthAccessTokenByOrigin[origin] = accessToken
66
+ } else if([ 'successfulLogout', 'unnecessaryLogout' ].includes(action)) {
67
+ delete sessionSyncAuthAccessTokenByOrigin[origin]
68
+ }
69
+ localStorage.setItem('sessionSyncAuthAccessTokenByOrigin', JSON.stringify(sessionSyncAuthAccessTokenByOrigin))
70
+ }
71
+
72
+ try {
73
+ callbacks[action] && callbacks[action]({ origin, accessToken, errorMessage })
74
+ } catch(err) {
75
+ console.error(err)
76
+ }
77
+
78
+ if(origin || accessToken || action || errorMessage) {
79
+ window.history.replaceState({}, null, `${location.href.replace(/\?.*$/, '')}${window.location.search.replace(/&?(?:origin|accessToken|action|errorMessage)=(?:[^&]*)/g, '').replace(/^\?$/, '')}`)
80
+ }
81
+
82
+ }
83
+
84
+ const sessionSyncAuth = {
85
+ getAccessToken,
86
+ logIn,
87
+ getUser,
88
+ logOut,
89
+ init,
90
+ setDefaultOrigin,
91
+ }
92
+
93
+ Object.freeze(sessionSyncAuth)
94
+
95
+ Object.defineProperty(window, 'sessionSyncAuth', {
96
+ value: sessionSyncAuth,
97
+ writable : false,
98
+ enumerable : true,
99
+ configurable : false
100
+ })
101
+
102
+ })();
@@ -0,0 +1,44 @@
1
+ const mysql = require('mysql')
2
+ const SqlString = require('mysql/lib/protocol/SqlString')
3
+ const { ConnectionString } = require('connection-string')
4
+ const util = require('util')
5
+
6
+ const setUpConnection = ({
7
+ connectionStr=`mysql://root@localhost/SessionSyncAuthSite`,
8
+ connectionObj,
9
+ }={}) => {
10
+
11
+ connectionObj = connectionObj || new ConnectionString(connectionStr)
12
+
13
+ global.sessionSyncAuthSiteConnection = mysql.createConnection({
14
+ multipleStatements: true,
15
+ dateStrings: true,
16
+ charset : 'utf8mb4',
17
+ queryFormat: function (query, values) {
18
+ if(!values) return query
19
+
20
+ if(/\:(\w+)/.test(query)) {
21
+ return query.replace(/\:(\w+)/g, (txt, key) => {
22
+ if(values.hasOwnProperty(key)) {
23
+ return this.escape(values[key])
24
+ }
25
+ return txt
26
+ })
27
+
28
+ } else {
29
+ return SqlString.format(query, values, this.config.stringifyObjects, this.config.timezone)
30
+ }
31
+ },
32
+ // debug: true,
33
+ host: connectionObj.host || connectionObj.hosts[0].name,
34
+ user: connectionObj.user || connectionObj.username,
35
+ password: connectionObj.password,
36
+ database: connectionObj.database || connectionObj.path[0],
37
+ port: connectionObj.port,
38
+ })
39
+
40
+ global.sessionSyncAuthSiteConnection.asyncQuery = util.promisify(global.sessionSyncAuthSiteConnection.query).bind(global.sessionSyncAuthSiteConnection)
41
+
42
+ }
43
+
44
+ module.exports = setUpConnection
@@ -0,0 +1,204 @@
1
+ const jwt = require('jsonwebtoken')
2
+
3
+ const iso6393To1 = require('./iso639-3To1')
4
+
5
+ const setUpSessionSyncAuthRoutes = ({
6
+ app,
7
+ siteId,
8
+ authDomain,
9
+ jwtSecret,
10
+ protocol='https',
11
+ paths: {
12
+ getUser='/get-user',
13
+ logIn='/log-in',
14
+ logOut='/log-out',
15
+ authSync='/auth-sync',
16
+ }={},
17
+ languageColType='639-3', // OPTIONS: '639-1', '639-3', 'IETF'
18
+ }) => {
19
+
20
+ app.get(getUser, (req, res, next) => {
21
+ res.json({
22
+ status: req.user ? 'Logged in' : 'Not logged in',
23
+ user: req.user,
24
+ })
25
+ })
26
+
27
+ app.get(logIn, (req, res, next) => {
28
+ const { loggedInRedirectUrl, cancelRedirectUrl } = req.query
29
+
30
+ const jwtData = jwt.sign(
31
+ {
32
+ cancelRedirectUrl,
33
+ loggedInRedirectUrl,
34
+ },
35
+ jwtSecret,
36
+ )
37
+
38
+ res.redirect(`${protocol}://${authDomain}/login?siteId=${encodeURIComponent(siteId)}&jwtData=${encodeURIComponent(jwtData)}`)
39
+ })
40
+
41
+ app.get(logOut, (req, res, next) => {
42
+ const { redirectUrl, noLoginRedirectUrl } = req.query
43
+
44
+ if(!req.user) {
45
+ console.warn(`logout attemped with no login`)
46
+ return res.redirect(noLoginRedirectUrl || redirectUrl)
47
+ }
48
+
49
+ const jwtData = jwt.sign(
50
+ {
51
+ userId: req.user.id,
52
+ redirectUrl,
53
+ },
54
+ jwtSecret,
55
+ )
56
+
57
+ res.redirect(`${protocol}://${authDomain}/logout?siteId=${encodeURIComponent(siteId)}&jwtData=${encodeURIComponent(jwtData)}`)
58
+ })
59
+
60
+ app.post(authSync, async (req, res, next) => {
61
+ const { payload } = req.body
62
+
63
+ try {
64
+
65
+ if(!global.sessionSyncAuthSiteConnection) {
66
+ throw new Error('You must include the authenticate() middleware prior to calling setUpSessionSyncAuthRoutes.')
67
+ }
68
+
69
+ const {
70
+ userTableName,
71
+ sessionTableName,
72
+ userTableColNameMap,
73
+ sessionTableColNameMap,
74
+ } = req.sessionSyncAuthSiteOptions
75
+
76
+ const { users } = jwt.verify(payload, jwtSecret)
77
+
78
+ if(!users) throw "Invalid payload. Missing `payload.user`."
79
+ if(!(users instanceof Array)) throw "Invalid payload. `payload.users` must be an array."
80
+
81
+ const queries = []
82
+ const variables = {}
83
+
84
+ const getSnakeCaseOfKeys = (obj, allowedKeys) => {
85
+ const newObj = {}
86
+ Object.keys(obj).forEach(key => {
87
+ const snakeCaseKey = key.replace(/[A-Z]/g, val => `_${val.toLowerCase()}`)
88
+ if(allowedKeys.includes(snakeCaseKey)) {
89
+ newObj[snakeCaseKey] = obj[key]
90
+ }
91
+ })
92
+ return newObj
93
+ }
94
+
95
+ const mapKeys = (obj, table) => {
96
+ let colNameMap = {}
97
+ if(table === 'user') colNameMap = userTableColNameMap
98
+ if(table === 'session') colNameMap = sessionTableColNameMap
99
+
100
+ for(let defaultCol in colNameMap) {
101
+ obj[colNameMap[defaultCol]] = obj[defaultCol]
102
+ delete obj[defaultCol]
103
+ }
104
+ }
105
+
106
+ const convertMsTimestampsToDateTimeStrings = obj => {
107
+ Object.keys(obj).forEach(key => {
108
+ if(/At$/.test(key)) {
109
+ obj[key] = new Date(obj[key]).toISOString().replace(/T/, ' ').replace(/Z/, '')
110
+ }
111
+ })
112
+ }
113
+
114
+ users.forEach(user => {
115
+ const { id, sessions, createdAt, updatedAt } = user
116
+
117
+ if(!id) throw "Invalid payload. Item in `payload.users` missing `id` key."
118
+ if(!sessions) throw "Invalid payload. Item in `payload.users` missing `sessions` key."
119
+ if(!(sessions instanceof Array)) throw "Invalid payload. `payload.users[].sessions` must be an array."
120
+ if(!createdAt) throw "Invalid payload. Item in `payload.users` missing `createdAt` key."
121
+ if(!updatedAt) throw "Invalid payload. Item in `payload.users` missing `updatedAt` key."
122
+
123
+ convertMsTimestampsToDateTimeStrings(user)
124
+
125
+ // add/update the user
126
+
127
+ variables[`user__${id}`] = getSnakeCaseOfKeys(
128
+ user,
129
+ [ 'id', 'email', 'name', 'image', 'language', 'created_at', 'updated_at' ],
130
+ )
131
+
132
+ if(languageColType !== 'IETF') {
133
+ variables[`user__${id}`].language = variables[`user__${id}`].language.split('-')[0]
134
+ }
135
+
136
+ if(languageColType === '639-1') {
137
+ variables[`user__${id}`].language = iso6393To1[variables[`user__${id}`].language] || 'en'
138
+ }
139
+
140
+ mapKeys(variables[`user__${id}`], 'user')
141
+
142
+ queries.push(`
143
+ INSERT INTO \`${userTableName}\` SET :user__${id}
144
+ ON DUPLICATE KEY UPDATE :user__${id}
145
+ `)
146
+
147
+ // add the sessions
148
+
149
+ sessions.forEach(session => {
150
+ const { accessToken, createdAt } = session
151
+
152
+ if(!accessToken) throw "Invalid payload. Item in `payload.users[].sessions` missing `accessToken` key."
153
+ if(!createdAt) throw "Invalid payload. Item in `payload.users[].sessions` missing `createdAt` key."
154
+
155
+ convertMsTimestampsToDateTimeStrings(session)
156
+
157
+ variables[`session__${accessToken}`] = {
158
+ ...getSnakeCaseOfKeys(
159
+ session,
160
+ [ 'created_at', 'access_token' ],
161
+ ),
162
+ user_id: id,
163
+ }
164
+ mapKeys(variables[`session__${accessToken}`], 'session')
165
+
166
+ queries.push(`
167
+ INSERT IGNORE INTO \`${sessionTableName}\` SET :session__${accessToken}
168
+ `)
169
+
170
+ })
171
+
172
+ // delete old sessions
173
+
174
+ variables[`userId__${id}`] = id
175
+ variables[`accessTokens__${id}`] = [
176
+ 'dummy_token', // so the SQL is still valid even if there are no sessions
177
+ ...sessions.map(({ accessToken }) => accessToken),
178
+ ]
179
+
180
+ queries.push(`
181
+ DELETE FROM \`${sessionTableName}\`
182
+ WHERE \`${(sessionTableColNameMap.user_id || `user_id`).replace(/`/g, '')}\` = :userId__${id}
183
+ AND \`${(sessionTableColNameMap.access_token || `access_token`).replace(/`/g, '')}\` NOT IN (:accessTokens__${id})
184
+ `)
185
+
186
+ })
187
+
188
+ await global.sessionSyncAuthSiteConnection.asyncQuery(
189
+ queries.join(';'),
190
+ variables,
191
+ )
192
+
193
+ res.json({ success: true })
194
+
195
+ } catch(err) {
196
+ console.log('Bad call to sync_api_endpoint', err)
197
+ res.status(400).send(`Invalid payload.`)
198
+ }
199
+
200
+ })
201
+
202
+ }
203
+
204
+ module.exports = setUpSessionSyncAuthRoutes
package/test/app.html ADDED
@@ -0,0 +1,82 @@
1
+ <!DOCTYPE html>
2
+
3
+ <html>
4
+
5
+ <head>
6
+ <script src="/sessionSyncAuthFrontend.js"></script>
7
+
8
+ <script>
9
+
10
+ window.sessionSyncAuth.init({
11
+ defaultOrigin: 'http://localhost:3002',
12
+ callbacks: {
13
+ canceledLogin: ({ origin }) => alert(`Canceled login to ${origin}`),
14
+ successfulLogin: ({ origin }) => alert(`Successful login to ${origin}`),
15
+ successfulLogout: ({ origin }) => alert(`Successful logout from ${origin}`),
16
+ unnecessaryLogout: ({ origin }) => alert(`Unnecessary logout from ${origin}`),
17
+ error: ({ errorMessage }) => alert(`ERROR: ${errorMessage}`),
18
+ },
19
+ })
20
+
21
+ const showAccessTokens = () => {
22
+ alert(localStorage.getItem('sessionSyncAuthAccessTokenByOrigin'))
23
+ }
24
+
25
+ const getUser = async options => {
26
+ const json = await window.sessionSyncAuth.getUser(options)
27
+ console.log(json)
28
+ alert(JSON.stringify(json, true))
29
+ }
30
+
31
+ </script>
32
+ </head>
33
+
34
+ <body>
35
+
36
+ <div>
37
+ <button
38
+ onclick="javascript:showAccessTokens();"
39
+ >
40
+ Show local access tokens
41
+ </button>
42
+ </div>
43
+
44
+ <div>
45
+ <button
46
+ onclick="javascript:window.sessionSyncAuth.logIn();"
47
+ >
48
+ Log in on 3002
49
+ </button>
50
+ <button
51
+ onclick="javascript:getUser();"
52
+ >
53
+ Get user on 3002
54
+ </button>
55
+ <button
56
+ onclick="javascript:window.sessionSyncAuth.logOut();"
57
+ >
58
+ Log out on 3002
59
+ </button>
60
+ </div>
61
+
62
+ <div>
63
+ <button
64
+ onclick="javascript:window.sessionSyncAuth.logIn({ origin: 'http://localhost:3003' });"
65
+ >
66
+ Log in on 3003
67
+ </button>
68
+ <button
69
+ onclick="javascript:getUser({ origin: 'http://localhost:3003' });"
70
+ >
71
+ Get user on 3003
72
+ </button>
73
+ <button
74
+ onclick="javascript:window.sessionSyncAuth.logOut({ origin: 'http://localhost:3003' });"
75
+ >
76
+ Log out on 3003
77
+ </button>
78
+ </div>
79
+
80
+ </body>
81
+
82
+ </html>
@@ -0,0 +1,115 @@
1
+ 'use strict'
2
+
3
+ const express = require('express')
4
+ const app = express()
5
+ const cors = require('cors')
6
+ const bodyParser = require('body-parser')
7
+
8
+ const authenticate = require('../src/authenticate')
9
+ const setUpSessionSyncAuthRoutes = require('../src/setUpSessionSyncAuthRoutes')
10
+
11
+ try {
12
+
13
+ const [
14
+ port,
15
+ dbPrefix,
16
+ authDomain,
17
+ x,
18
+ ] = process.argv.slice(2)
19
+
20
+ if(port === undefined) {
21
+ throw new Error(`NO_PARAMS`)
22
+ }
23
+
24
+ if(x !== undefined) {
25
+ throw new Error(`BAD_PARAMS`)
26
+ }
27
+
28
+ // Middleware
29
+
30
+ app.use(cors())
31
+
32
+ if(dbPrefix) {
33
+ app.use(bodyParser.json())
34
+ app.use(authenticate({
35
+ // connectionObj or connectionStr required for all but localhost testing
36
+ userTableName: `${dbPrefix}_users`,
37
+ sessionTableName: `${dbPrefix}_sessions`,
38
+ }))
39
+ }
40
+
41
+ // API
42
+
43
+ if(!dbPrefix) {
44
+
45
+ app.get('/', (req, res, next) => {
46
+ res.sendFile('app.html', {root: __dirname })
47
+ })
48
+
49
+ app.get('/sessionSyncAuthFrontend.js', (req, res, next) => {
50
+ res.sendFile('sessionSyncAuthFrontend.js', {root: `${__dirname}/../src` })
51
+ })
52
+
53
+ }
54
+
55
+ if(dbPrefix) {
56
+ setUpSessionSyncAuthRoutes({
57
+ app,
58
+ siteId: port,
59
+ authDomain: authDomain || `localhost:3005`,
60
+ jwtSecret: `secret:${port}`,
61
+ protocol: authDomain ? `https` : `http`,
62
+ languageColType: '639-1',
63
+ })
64
+ }
65
+
66
+ // Error handler
67
+ app.use((err, req, res, next) => {
68
+ console.error(err)
69
+ res.status(500).send('Internal Serverless Error')
70
+ })
71
+
72
+ // Local listener
73
+ app.listen(port, (err) => {
74
+ if (err) throw err
75
+ console.log(`> Ready on http://localhost:${port}`)
76
+ })
77
+
78
+ } catch(err) {
79
+
80
+ const logSyntax = () => {
81
+ console.log(`Syntax: \`npm run dev [port] [dbPrefix] [authDomain]\`\n`)
82
+ console.log(`Example #1: \`npm run 3002 db1\``)
83
+ console.log(`Example #2: \`npm run 3003 db2 auth.staging.resourcingeducation.com\``)
84
+ console.log(``)
85
+ }
86
+
87
+ switch(err.message.split(',')[0]) {
88
+
89
+ case `NO_PARAMS`: {
90
+ logSyntax()
91
+ break
92
+ }
93
+
94
+ case `BAD_PARAMS`: {
95
+ console.log(`\n-----------------------`)
96
+ console.log(`ERROR: Bad parameters.`)
97
+ console.log(`-----------------------\n`)
98
+ logSyntax()
99
+ break
100
+ }
101
+
102
+ default: {
103
+ console.log(`\n-----------------------`)
104
+ console.log(`\nERROR: ${err.message}\n`)
105
+ console.log(`-----------------------\n`)
106
+ logSyntax()
107
+ }
108
+
109
+ }
110
+
111
+ process.exit()
112
+
113
+ }
114
+
115
+ module.exports = app