This repository has been archived by the owner on Jul 15, 2020. It is now read-only.
/
auth.js
174 lines (146 loc) · 5.58 KB
/
auth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/**
* Add routes for authentication
*
* Also sets up dependancies for authentication:
* - Adds sessions support to Express (with HTTP only cookies for security)
* - Configures session store (defaults to a flat file store in /tmp/sessions)
* - Adds protection for Cross Site Request Forgery attacks to all POST requests
*
* Normally some of this logic might be elsewhere (like server.js) but for the
* purposes of this example all server logic related to authentication is here.
*/
"use strict"
const bodyParser = require('body-parser')
const session = require('express-session')
const FileStore = require('session-file-store')(session)
const nodemailer = require('nodemailer')
const csrf = require('lusca').csrf()
const uuid = require('uuid/v4')
exports.configure = (app, server, options) => {
if (!options) options = {}
if (!options.db || !options.db.models || !options.db.models.user)
throw new Error("Database with user model is a required option!")
const User = options.db.models.user
// Base path for auth URLs
const path = options.path || '/auth'
// Directory for auth pages
const pages = options.pages || 'auth'
// The secret is used to encrypt/decrypt sessions (you should pass your own!)
const secret = options.secret || 'AAAA-BBBB-CCCC-DDDD'
// Configure session store (defaults to using file system)
const store = options.store || new FileStore({ path: '/tmp/sessions', secret: secret })
// Max session age in ms (default is 4 weeks)
// NB: With 'rolling: true' passed to session() the session expiry time will
// be reset every time a user visits the site again before it expires.
const maxAge = options.maxAge || 60000 * 60 * 24 * 7 * 4
// How often the client should revalidate the session in ms (default 60s)
// Does not impact the session life on the server, but causes the client to
// always refetch session info after N seconds has elapsed since last checked.
// Sensible values are between 0 (always check the server) and a few minutes.
const clientMaxAge = options.clientMaxAge || 60000
// URL of the server (e.g. "http://www.example.com"). Used when sending
// sign-in emails. Autodetects current server hostname / domain if null.
const serverUrl = options.serverUrl || null
// Mailserver (defaults to sending from localhost if null)
const mailserver = options.mailserver || null
// Load body parser to handle POST requests
server.use(bodyParser.json())
server.use(bodyParser.urlencoded({ extended: true }))
// Configure sessions
server.use(session({
secret: secret,
store: store,
resave: false,
rolling: true,
saveUninitialized: true,
httpOnly: true,
cookie: {
maxAge: maxAge
}
}))
// Add CSRF to all POST requests
// (If you want to add exceptions to paths you can do that here)
server.use((req, res, next) => {
csrf(req, res, next)
})
// Add route to get CSRF token via AJAX
server.get(path+'/csrf', (req, res) => {
return res.json({ csrfToken: res.locals._csrf })
})
// Return session info
server.get(path+'/session', (req, res) => {
// @TODO Instead of storing the "user" object in the sesssion, we should
// really just store the User ID and fetch the User object.
return res.json({
user: req.session.user || null,
isLoggedIn: (req.session.user) ? true : false,
clientMaxAge: clientMaxAge,
csrfToken: res.locals._csrf
})
})
// On post request, redirect to page with instrutions to check email for link
server.post(path+'/signin', (req, res) => {
const email = req.body.email || null
if (!email || email.trim() == '')
return app.render(req, res, pages+'/signin', req.params)
const token = uuid()
const verificationUrl = (serverUrl || "http://"+req.headers.host)+'/auth/signin/'+token
// Create verification token save it to database
// @TODO Error handling (i.e. don't send email unless it worked)
User.one({ email: email }, function(err, user) {
if (user) {
user.token = token
user.save(function(err) {
// if (err) throw err
})
} else {
User.create({ email: email, token: token }, function(err) {
// if (err) throw err
})
}
})
nodemailer
.createTransport(mailserver)
.sendMail({
to: email,
from: "noreply@"+req.headers.host.split(":")[0],
subject: 'Sign in link',
text: 'Use the link below to sign in:\n\n'+
verificationUrl+'\n\n'
}, function(err) {
// @TODO Handle errors
if (err) console.log("Error sending email", err)
return app.render(req, res, pages+'/check-email', req.params)
})
})
server.get(path+'/signin/:token', (req, res) => {
if (!req.params.token)
return res.redirect(path+'/signin')
User.one({ token: req.params.token }, function(err, user) {
if (user) {
// Reset token and mark as verified
user.token = null
user.verified = true
user.save(function(err) {
// if (err) throw err
req.session.user = user
return res.redirect(path+'/valid')
})
} else {
return res.redirect(path+'/invalid')
}
})
})
server.post(path+'/signout', (req, res) => {
// Log the user out by setting isLoggedIn to false and removing user
// object from the session
req.session.isLoggedIn = false
if (req.session.user)
delete req.session.user
res.redirect('/')
})
}
// This method works better for URLs than the default RegEx.escape method
const escape = function(s) {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
}