diff options
author | Johannes Zellner <johannes@cloudron.io> | 2017-02-09 12:40:40 +0100 |
---|---|---|
committer | Johannes Zellner <johannes@cloudron.io> | 2017-02-09 12:40:40 +0100 |
commit | 4a27fce742a75881cd84607f4237624d8c0a0a22 (patch) | |
tree | 231baf61d87005c807803b37aed57147630a0501 | |
parent | 3422a21b8eb26682772867e1fd997ef806229459 (diff) | |
download | Surfer-4a27fce742a75881cd84607f4237624d8c0a0a22.tar.gz Surfer-4a27fce742a75881cd84607f4237624d8c0a0a22.tar.zst Surfer-4a27fce742a75881cd84607f4237624d8c0a0a22.zip |
Use accessTokens instead of username/password
-rw-r--r-- | frontend/js/app.js | 57 | ||||
-rw-r--r-- | npm-shrinkwrap.json | 15 | ||||
-rw-r--r-- | package.json | 4 | ||||
-rwxr-xr-x | server.js | 3 | ||||
-rw-r--r-- | src/auth.js | 65 |
5 files changed, 112 insertions, 32 deletions
diff --git a/frontend/js/app.js b/frontend/js/app.js index 33346aa..b07560a 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js | |||
@@ -1,37 +1,54 @@ | |||
1 | (function () { | 1 | (function () { |
2 | 'use strict'; | 2 | 'use strict'; |
3 | 3 | ||
4 | function getProfile(accessToken, callback) { | ||
5 | callback = callback || function (error) { if (error) console.error(error); }; | ||
6 | |||
7 | superagent.get('/api/profile').query({ access_token: accessToken }).end(function (error, result) { | ||
8 | app.busy = false; | ||
9 | |||
10 | if (error && !error.response) return callback(error); | ||
11 | if (result.statusCode !== 200) { | ||
12 | delete localStorage.accessToken; | ||
13 | return callback('Invalid access token'); | ||
14 | } | ||
15 | |||
16 | localStorage.accessToken = accessToken; | ||
17 | app.session.username = result.body.username; | ||
18 | app.session.valid = true; | ||
19 | |||
20 | callback(); | ||
21 | }); | ||
22 | } | ||
23 | |||
4 | function login(username, password) { | 24 | function login(username, password) { |
5 | username = username || app.loginData.username; | 25 | username = username || app.loginData.username; |
6 | password = password || app.loginData.password; | 26 | password = password || app.loginData.password; |
7 | 27 | ||
8 | app.busy = true; | 28 | app.busy = true; |
9 | 29 | ||
10 | superagent.get('/api/files/').query({ username: username, password: password }).end(function (error, result) { | 30 | superagent.post('/api/login').query({ username: username, password: password }).end(function (error, result) { |
11 | app.busy = false; | 31 | app.busy = false; |
12 | 32 | ||
13 | if (error) return console.error(error); | 33 | if (error) return console.error(error); |
14 | if (result.statusCode === 401) return console.error('Invalid credentials'); | 34 | if (result.statusCode === 401) return console.error('Invalid credentials'); |
15 | 35 | ||
16 | app.session.valid = true; | 36 | getProfile(result.body.accessToken, function (error) { |
17 | app.session.username = username; | 37 | if (error) return console.error(error); |
18 | app.session.password = password; | ||
19 | |||
20 | // clearly not the best option | ||
21 | localStorage.username = username; | ||
22 | localStorage.password = password; | ||
23 | 38 | ||
24 | loadDirectory(window.location.hash.slice(1)); | 39 | loadDirectory(window.location.hash.slice(1)); |
40 | }); | ||
25 | }); | 41 | }); |
26 | } | 42 | } |
27 | 43 | ||
28 | function logout() { | 44 | function logout() { |
29 | app.session.valid = false; | 45 | superagent.post('/api/logout').query({ access_token: localStorage.accessToken }).end(function (error) { |
30 | app.session.username = null; | 46 | if (error) console.error(error); |
31 | app.session.password = null; | 47 | |
48 | app.session.valid = false; | ||
32 | 49 | ||
33 | delete localStorage.username; | 50 | delete localStorage.accessToken; |
34 | delete localStorage.password; | 51 | }); |
35 | } | 52 | } |
36 | 53 | ||
37 | function sanitize(filePath) { | 54 | function sanitize(filePath) { |
@@ -77,7 +94,7 @@ function loadDirectory(filePath) { | |||
77 | 94 | ||
78 | filePath = filePath ? sanitize(filePath) : '/'; | 95 | filePath = filePath ? sanitize(filePath) : '/'; |
79 | 96 | ||
80 | superagent.get('/api/files/' + encode(filePath)).query({ username: app.session.username, password: app.session.password }).end(function (error, result) { | 97 | superagent.get('/api/files/' + encode(filePath)).query({ access_token: localStorage.accessToken }).end(function (error, result) { |
81 | app.busy = false; | 98 | app.busy = false; |
82 | 99 | ||
83 | if (result && result.statusCode === 401) return logout(); | 100 | if (result && result.statusCode === 401) return logout(); |
@@ -138,7 +155,7 @@ function uploadFiles(files) { | |||
138 | var formData = new FormData(); | 155 | var formData = new FormData(); |
139 | formData.append('file', file); | 156 | formData.append('file', file); |
140 | 157 | ||
141 | superagent.post('/api/files' + path).query({ username: app.session.username, password: app.session.password }).send(formData).end(function (error, result) { | 158 | superagent.post('/api/files' + path).query({ access_token: localStorage.accessToken }).send(formData).end(function (error, result) { |
142 | if (result && result.statusCode === 401) return logout(); | 159 | if (result && result.statusCode === 401) return logout(); |
143 | if (result && result.statusCode !== 201) console.error('Error uploading file: ', result.statusCode); | 160 | if (result && result.statusCode !== 201) console.error('Error uploading file: ', result.statusCode); |
144 | if (error) console.error(error); | 161 | if (error) console.error(error); |
@@ -189,7 +206,7 @@ function del(entry) { | |||
189 | 206 | ||
190 | var path = encode(sanitize(app.path + '/' + entry.filePath)); | 207 | var path = encode(sanitize(app.path + '/' + entry.filePath)); |
191 | 208 | ||
192 | superagent.del('/api/files' + path).query({ username: app.session.username, password: app.session.password, recursive: true }).end(function (error, result) { | 209 | superagent.del('/api/files' + path).query({ access_token: localStorage.accessToken, recursive: true }).end(function (error, result) { |
193 | app.busy = false; | 210 | app.busy = false; |
194 | 211 | ||
195 | if (result && result.statusCode === 401) return logout(); | 212 | if (result && result.statusCode === 401) return logout(); |
@@ -216,7 +233,7 @@ function rename(data) { | |||
216 | var path = encode(sanitize(app.path + '/' + data.entry.filePath)); | 233 | var path = encode(sanitize(app.path + '/' + data.entry.filePath)); |
217 | var newFilePath = sanitize(app.path + '/' + data.newFilePath); | 234 | var newFilePath = sanitize(app.path + '/' + data.newFilePath); |
218 | 235 | ||
219 | superagent.put('/api/files' + path).query({ username: app.session.username, password: app.session.password }).send({ newFilePath: newFilePath }).end(function (error, result) { | 236 | superagent.put('/api/files' + path).query({ access_token: localStorage.accessToken }).send({ newFilePath: newFilePath }).end(function (error, result) { |
220 | app.busy = false; | 237 | app.busy = false; |
221 | 238 | ||
222 | if (result && result.statusCode === 401) return logout(); | 239 | if (result && result.statusCode === 401) return logout(); |
@@ -241,7 +258,7 @@ function createDirectory(name) { | |||
241 | 258 | ||
242 | var path = encode(sanitize(app.path + '/' + name)); | 259 | var path = encode(sanitize(app.path + '/' + name)); |
243 | 260 | ||
244 | superagent.post('/api/files' + path).query({ username: app.session.username, password: app.session.password, directory: true }).end(function (error, result) { | 261 | superagent.post('/api/files' + path).query({ access_token: localStorage.accessToken, directory: true }).end(function (error, result) { |
245 | app.busy = false; | 262 | app.busy = false; |
246 | 263 | ||
247 | if (result && result.statusCode === 401) return logout(); | 264 | if (result && result.statusCode === 401) return logout(); |
@@ -327,7 +344,7 @@ var app = new Vue({ | |||
327 | 344 | ||
328 | window.app = app; | 345 | window.app = app; |
329 | 346 | ||
330 | login(localStorage.username, localStorage.password); | 347 | getProfile(localStorage.accessToken); |
331 | 348 | ||
332 | $(window).on('hashchange', function () { | 349 | $(window).on('hashchange', function () { |
333 | loadDirectory(window.location.hash.slice(1)); | 350 | loadDirectory(window.location.hash.slice(1)); |
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 6d34f16..ff96bbb 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json | |||
@@ -903,6 +903,11 @@ | |||
903 | } | 903 | } |
904 | } | 904 | } |
905 | }, | 905 | }, |
906 | "passport-http-bearer": { | ||
907 | "version": "1.0.1", | ||
908 | "from": "passport-http-bearer@latest", | ||
909 | "resolved": "https://registry.npmjs.org/passport-http-bearer/-/passport-http-bearer-1.0.1.tgz" | ||
910 | }, | ||
906 | "passport-ldapjs": { | 911 | "passport-ldapjs": { |
907 | "version": "1.0.2", | 912 | "version": "1.0.2", |
908 | "from": "passport-ldapjs@>=1.0.2 <2.0.0", | 913 | "from": "passport-ldapjs@>=1.0.2 <2.0.0", |
@@ -1016,6 +1021,11 @@ | |||
1016 | } | 1021 | } |
1017 | } | 1022 | } |
1018 | }, | 1023 | }, |
1024 | "passport-strategy": { | ||
1025 | "version": "1.0.0", | ||
1026 | "from": "passport-strategy@>=1.0.0 <2.0.0", | ||
1027 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz" | ||
1028 | }, | ||
1019 | "readline-sync": { | 1029 | "readline-sync": { |
1020 | "version": "1.4.1", | 1030 | "version": "1.4.1", |
1021 | "from": "readline-sync@>=1.4.1 <2.0.0", | 1031 | "from": "readline-sync@>=1.4.1 <2.0.0", |
@@ -1534,6 +1544,11 @@ | |||
1534 | "version": "1.8.3", | 1544 | "version": "1.8.3", |
1535 | "from": "underscore@>=1.8.3 <2.0.0", | 1545 | "from": "underscore@>=1.8.3 <2.0.0", |
1536 | "resolved": "http://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz" | 1546 | "resolved": "http://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz" |
1547 | }, | ||
1548 | "uuid": { | ||
1549 | "version": "3.0.1", | ||
1550 | "from": "uuid@latest", | ||
1551 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz" | ||
1537 | } | 1552 | } |
1538 | } | 1553 | } |
1539 | } | 1554 | } |
diff --git a/package.json b/package.json index 55bfd4b..e2e0488 100644 --- a/package.json +++ b/package.json | |||
@@ -37,13 +37,15 @@ | |||
37 | "morgan": "^1.7.0", | 37 | "morgan": "^1.7.0", |
38 | "multiparty": "^4.1.2", | 38 | "multiparty": "^4.1.2", |
39 | "passport": "^0.2.2", | 39 | "passport": "^0.2.2", |
40 | "passport-http-bearer": "^1.0.1", | ||
40 | "passport-ldapjs": "^1.0.2", | 41 | "passport-ldapjs": "^1.0.2", |
41 | "readline-sync": "^1.4.1", | 42 | "readline-sync": "^1.4.1", |
42 | "request": "^2.69.0", | 43 | "request": "^2.69.0", |
43 | "safetydance": "^0.1.1", | 44 | "safetydance": "^0.1.1", |
44 | "serve-index": "^1.8.0", | 45 | "serve-index": "^1.8.0", |
45 | "superagent": "^1.7.2", | 46 | "superagent": "^1.7.2", |
46 | "underscore": "^1.8.3" | 47 | "underscore": "^1.8.3", |
48 | "uuid": "^3.0.1" | ||
47 | }, | 49 | }, |
48 | "devDependencies": { | 50 | "devDependencies": { |
49 | "expect.js": "^0.3.1", | 51 | "expect.js": "^0.3.1", |
@@ -23,6 +23,9 @@ var router = new express.Router(); | |||
23 | 23 | ||
24 | var multipart = multipart({ maxFieldsSize: 2 * 1024, limit: '512mb', timeout: 3 * 60 * 1000 }); | 24 | var multipart = multipart({ maxFieldsSize: 2 * 1024, limit: '512mb', timeout: 3 * 60 * 1000 }); |
25 | 25 | ||
26 | router.post ('/api/login', auth.login); | ||
27 | router.post ('/api/logout', auth.verify, auth.logout); | ||
28 | router.get ('/api/profile', auth.verify, auth.getProfile); | ||
26 | router.get ('/api/files/*', auth.verify, files.get); | 29 | router.get ('/api/files/*', auth.verify, files.get); |
27 | router.post ('/api/files/*', auth.verify, multipart, files.post); | 30 | router.post ('/api/files/*', auth.verify, multipart, files.post); |
28 | router.put ('/api/files/*', auth.verify, files.put); | 31 | router.put ('/api/files/*', auth.verify, files.put); |
diff --git a/src/auth.js b/src/auth.js index b56f09f..8645d4c 100644 --- a/src/auth.js +++ b/src/auth.js | |||
@@ -4,10 +4,25 @@ var passport = require('passport'), | |||
4 | path = require('path'), | 4 | path = require('path'), |
5 | safe = require('safetydance'), | 5 | safe = require('safetydance'), |
6 | bcrypt = require('bcryptjs'), | 6 | bcrypt = require('bcryptjs'), |
7 | LdapStrategy = require('passport-ldapjs').Strategy; | 7 | uuid = require('uuid/v4'), |
8 | BearerStrategy = require('passport-http-bearer').Strategy, | ||
9 | LdapStrategy = require('passport-ldapjs').Strategy, | ||
10 | HttpSuccess = require('connect-lastmile').HttpSuccess; | ||
8 | 11 | ||
9 | var LOCAL_AUTH_FILE = path.resolve(process.env.LOCAL_AUTH_FILE || './.users.json'); | 12 | var LOCAL_AUTH_FILE = path.resolve(process.env.LOCAL_AUTH_FILE || './.users.json'); |
10 | 13 | ||
14 | var gTokenStore = {}; | ||
15 | |||
16 | function issueAccessToken() { | ||
17 | return function (req, res, next) { | ||
18 | var accessToken = uuid(); | ||
19 | |||
20 | gTokenStore[accessToken] = req.user; | ||
21 | |||
22 | next(new HttpSuccess(201, { accessToken: accessToken, user: req.user })); | ||
23 | }; | ||
24 | } | ||
25 | |||
11 | passport.serializeUser(function (user, done) { | 26 | passport.serializeUser(function (user, done) { |
12 | console.log('serializeUser', user); | 27 | console.log('serializeUser', user); |
13 | done(null, user.uid); | 28 | done(null, user.uid); |
@@ -24,20 +39,28 @@ var LDAP_USERS_BASE_DN = process.env.LDAP_USERS_BASE_DN; | |||
24 | if (LDAP_URL && LDAP_USERS_BASE_DN) { | 39 | if (LDAP_URL && LDAP_USERS_BASE_DN) { |
25 | console.log('Enable ldap auth'); | 40 | console.log('Enable ldap auth'); |
26 | 41 | ||
27 | exports.verify = passport.authenticate('ldap'); | 42 | exports.login = [ passport.authenticate('ldap'), issueAccessToken() ]; |
28 | } else { | 43 | } else { |
29 | console.log('Use local user file:', LOCAL_AUTH_FILE); | 44 | console.log('Use local user file:', LOCAL_AUTH_FILE); |
30 | 45 | ||
31 | exports.verify = function (req, res, next) { | 46 | exports.login = [ |
32 | var users = safe.JSON.parse(safe.fs.readFileSync(LOCAL_AUTH_FILE)); | 47 | function (req, res, next) { |
33 | if (!users) return res.send(401); | 48 | var users = safe.JSON.parse(safe.fs.readFileSync(LOCAL_AUTH_FILE)); |
34 | if (!users[req.query.username]) return res.send(401); | 49 | if (!users) return res.send(401); |
50 | if (!users[req.query.username]) return res.send(401); | ||
35 | 51 | ||
36 | bcrypt.compare(req.query.password, users[req.query.username].passwordHash, function (error, valid) { | 52 | bcrypt.compare(req.query.password, users[req.query.username].passwordHash, function (error, valid) { |
37 | if (error || !valid) return res.send(401); | 53 | if (error || !valid) return res.send(401); |
38 | next(); | 54 | |
39 | }); | 55 | req.user = { |
40 | }; | 56 | username: req.query.username |
57 | }; | ||
58 | |||
59 | next(); | ||
60 | }); | ||
61 | }, | ||
62 | issueAccessToken() | ||
63 | ]; | ||
41 | } | 64 | } |
42 | 65 | ||
43 | var opts = { | 66 | var opts = { |
@@ -58,3 +81,23 @@ var opts = { | |||
58 | passport.use(new LdapStrategy(opts, function (profile, done) { | 81 | passport.use(new LdapStrategy(opts, function (profile, done) { |
59 | done(null, profile); | 82 | done(null, profile); |
60 | })); | 83 | })); |
84 | |||
85 | exports.verify = passport.authenticate('bearer', { session: false }); | ||
86 | |||
87 | passport.use(new BearerStrategy(function (token, done) { | ||
88 | if (!gTokenStore[token]) return done(null, false); | ||
89 | |||
90 | return done(null, gTokenStore[token], { accessToken: token }); | ||
91 | })); | ||
92 | |||
93 | exports.logout = function (req, res, next) { | ||
94 | console.log(req.authInfo); | ||
95 | |||
96 | delete gTokenStore[req.authInfo.accessToken]; | ||
97 | |||
98 | next(new HttpSuccess(200, {})); | ||
99 | }; | ||
100 | |||
101 | exports.getProfile = function (req, res, next) { | ||
102 | next(new HttpSuccess(200, { username: req.user.username })); | ||
103 | }; | ||