-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
182 lines (166 loc) · 8.22 KB
/
index.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
175
176
177
178
179
180
181
182
const util = require('util')
const assert = require('assert')
const URL = require('url').URL
const axios = require('axios')
const jwt = require('jsonwebtoken')
const jwksRsa = require('jwks-rsa')
const Cookies = require('cookies')
jwt.verifyAsync = util.promisify(jwt.verify)
const debug = require('debug')('session')
module.exports = ({ directoryUrl, privateDirectoryUrl, publicUrl, cookieName, cookieDomain, sameSite }) => {
assert.ok(!!directoryUrl, 'directoryUrl parameter is required')
assert.ok(!publicUrl, 'publicUrl parameter is deprecated')
assert.ok(!cookieDomain, 'cookieDomain parameter is deprecated')
assert.ok(!sameSite, 'sameSite parameter is deprecated')
cookieName = cookieName || 'id_token'
debug('Init with parameters', { directoryUrl, cookieName })
privateDirectoryUrl = privateDirectoryUrl || directoryUrl
const jwksClient = getJWKSClient(privateDirectoryUrl)
// This middleware checks if a user has an active session with a valid token
// it defines req.user and it can extend the session if necessary.
const auth = asyncWrap(async (req, res, next) => {
// JWT in a cookie = already active session
const cookies = new Cookies(req, res)
const token = getCookieToken(cookies, req, cookieName)
if (token) {
try {
debug(`Verify JWT token from the ${cookieName} cookie`)
req.user = await verifyToken(jwksClient, token)
if (req.user.temporary) throw new Error('Temporary tokens should not be used in actual auth cookies')
readOrganization(cookies, cookieName, req, req.user)
debug('JWT token from cookie is ok', req.user)
} catch (err) {
// Token expired or bad in another way.. delete the cookie
console.warn('JWT token from cookie is broken', err)
cookies.set(cookieName, null)
cookies.set(cookieName + '_sign', null)
cookies.set(cookieName + '_org', null)
cookies.set(cookieName + '_dep', null)
}
}
next()
})
const requiredAuth = (req, res, next) => {
auth(req, res, err => {
if (err) return next(err)
if (!req.user) return res.status(401).send()
next()
})
}
return { auth, requiredAuth, verifyToken: (token) => verifyToken(jwksClient, token) }
}
// A cache of jwks clients, so that this module's main function can be called multiple times
const jwksClients = {}
function getJWKSClient (directoryUrl) {
if (jwksClients[directoryUrl]) return jwksClients[directoryUrl]
jwksClients[directoryUrl] = jwksRsa({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: directoryUrl + '/.well-known/jwks.json'
})
jwksClients[directoryUrl].getSigningKeyAsync = util.promisify(jwksClients[directoryUrl].getSigningKey)
return jwksClients[directoryUrl]
}
// Fetch a session token from cookies if the same site policy is respected
function getCookieToken (cookies, req, cookieName) {
let token = cookies.get(cookieName)
if (!token) return null
const signature = cookies.get(cookieName + '_sign')
token += '.' + signature
return token
}
// Fetch the public info of signing key from the directory that acts as jwks provider
async function verifyToken (jwksClient, token) {
const decoded = jwt.decode(token, { complete: true })
const signingKey = await jwksClient.getSigningKeyAsync(decoded.header.kid)
return jwt.verifyAsync(token, signingKey.publicKey || signingKey.rsaPublicKey)
}
// Use complementary cookie id_token_org to set the current active organization of the user
// also set consumerFlag that is used by applications to decide if they should ask confirmation to the user
// of the right quotas or other organization related context to apply
// it is 'user' if id_token_org is an empty string or is equal to 'user'
// it is null if id_token_org is absent or if it does not match an organization of the current user
// it is the id of the orga in id_token_org
function readOrganization (cookies, cookieName, req, user) {
if (!user) return
// The order is important. The header can set explicitly on a query even if the cookie contradicts.
const organizationId = req.headers['x-organizationid'] ? req.headers['x-organizationid'].split(':')[0] : cookies.get(cookieName + '_org')
// we use decodeURIComponent on _dep cookie as older departments could have spacial chars (no longer, we use a slug now) and some client cookies libraries use encodeURIComponent
const departmentId = req.headers['x-organizationid'] ? req.headers['x-organizationid'].split(':')[1] : (cookies.get(cookieName + '_dep') && decodeURIComponent(cookies.get(cookieName + '_dep')))
user.activeAccount = { type: 'user', id: user.id, name: user.name }
user.accountOwner = { ...user.activeAccount }
user.accountOwnerRole = 'admin'
if (organizationId) {
user.organization = (user.organizations || []).find(o => o.id === organizationId)
if (departmentId) {
user.organization = (user.organizations || []).find(o => o.id === organizationId && o.department === departmentId)
}
if (user.organization) {
user.consumerFlag = user.organization.id
user.activeAccount = { ...user.organization, type: 'organization' }
user.accountOwner = { type: 'organization', id: user.organization.id, name: user.organization.name }
if (user.organization.department) {
user.accountOwner.department = user.organization.department
if (user.organization.departmentName) {
user.accountOwner.departmentName = user.organization.departmentName
}
}
user.accountOwnerRole = user.organization.role
} else if (organizationId === '' || organizationId.toLowerCase() === 'user') {
user.consumerFlag = 'user'
}
}
}
// Exchange a token (because if was a temporary auth token of because it is too old)
/* async function _exchangeToken (privateDirectoryUrl, token, params) {
const exchangeRes = await axios.post(privateDirectoryUrl + '/api/auth/exchange', null, { headers: { Authorization: 'Bearer ' + token }, params })
return exchangeRes.data
} */
// small route wrapper for better use of async/await with express
function asyncWrap (route) {
return (req, res, next) => route(req, res, next).catch(next)
}
// Adding a few things for testing purposes
module.exports.maildevAuth = async (email, sdUrl = 'http://localhost:8080', maildevUrl = 'http://localhost:1080', org) => {
await axios.post(sdUrl + '/api/auth/passwordless', { email }, { params: { redirect: sdUrl + '?id_token=', org } })
const emails = (await axios.get(maildevUrl + '/email')).data
const host = new URL(sdUrl).host
const emailObj = emails
.reverse()
.find(e => e.subject.indexOf(host) !== -1 && e.to[0].address.toLowerCase() === email.toLowerCase())
if (!emailObj) throw new Error('Failed to find email sent to ' + email)
const match = emailObj.text.split('\n').find(l => l.startsWith(sdUrl))
if (!match) throw new Error('Failed to extract id_token from mail content')
return match
}
module.exports.passwordAuth = async (email, password, sdUrl = 'http://localhost:8080', adminMode = false, org) => {
const res = await axios.post(sdUrl + '/api/auth/password', { email, password, adminMode, org }, { params: { redirect: sdUrl + '?id_token=' }, maxRedirects: 0 })
return res.data
}
const _axiosInstances = {}
module.exports.axiosAuth = async (email, org, opts = {}, sdUrl = 'http://localhost:8080', maildevUrl = 'http://localhost:1080') => {
if (!email) {
_axiosInstances.anonymous = axios.create(opts)
return _axiosInstances.anonymous
}
if (_axiosInstances[email]) return _axiosInstances[email]
let callbackUrl
if (email.indexOf(':') !== -1) {
callbackUrl = await module.exports.passwordAuth(email.split(':')[0], email.split(':')[1], sdUrl, email.split(':').includes('adminMode'), org)
} else {
callbackUrl = await module.exports.maildevAuth(email, sdUrl, maildevUrl, org)
}
if (callbackUrl.startsWith(sdUrl + '/simple-directory')) {
callbackUrl = callbackUrl.replace(sdUrl + '/simple-directory', sdUrl)
}
try {
await axios.get(callbackUrl, { maxRedirects: 0 })
} catch (err) {
if (!err.response || err.response.status !== 302) throw err
opts.headers = opts.headers || {}
opts.headers.Cookie = err.response.headers['set-cookie'].map(s => s.split(';')[0]).join(';')
}
_axiosInstances[email] = axios.create(opts)
return _axiosInstances[email]
}