-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathproviders.js
273 lines (232 loc) · 8.65 KB
/
providers.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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
const passport = require('passport')
const R = require('ramda')
const spMetadata = require('./sp-meta')
const misc = require('./utils/misc')
const logger = require('./utils/logging')
const extraPassportParams = require('./extra-passport-params')
const cacheProvider = require('./cache-provider')
let prevConfigHash = 0
// These are the (node) strategies loaded so far: [{id: "...", Strategy: ...}, ... ]
const passportStrategies = []
function applyMapping (profile, provider) {
let mappedProfile
try {
const mapping = global.providers.find(
providerObj => providerObj.id === provider).mapping
const additionalParams = profile.extras
delete profile.extras
logger.log2('silly', `Raw profile is ${JSON.stringify(profile._json)}`)
logger.log2('info', `Applying mapping '${mapping}' to profile`)
mappedProfile = require('./mappings/' + mapping)(profile, additionalParams)
logger.log2('debug', `Resulting profile data is\n${JSON.stringify(mappedProfile, null, 4)}`)
} catch (err) {
logger.log2('error', `An error occurred: ${err}`)
}
return mappedProfile
}
function getVerifyFunction (provider) {
const arity = provider.verifyCallbackArity
const uncurried = (...args) => {
// profile and callback are the last 2 params in all passport verify functions,
// except for passport-openidconnect which does not follow this convention
let profile, extras
if (provider.passportStrategyId === 'passport-openidconnect') {
// Check passport-openidconnect/lib/strategy.js
const index = provider.options.passReqToCallback ? 1 : 0
profile = args[2 + index]
extras = args.slice(0, 2 + index)
extras = extras.concat(args.slice(3 + index, arity - 1))
} else {
profile = args[arity - 2]
extras = args.slice(0, arity - 2)
}
const cb = args[arity - 1]
profile.providerKey = provider.id
profile.extras = extras
return cb(null, profile)
}
// guarantee the function has the arity required
return R.curryN(arity, uncurried)
}
function setupStrategy (provider) {
logger.log2('info', `Setting up strategy for provider ${provider.displayName}`)
logger.log2('debug', `Provider data is\n${JSON.stringify(provider, null, 4)}`)
const id = provider.id
const strategyModule = provider.passportStrategyId
let Strategy = passportStrategies.find(strategy => strategy.id === id)
// if strategyModule is not found, load it
if (Strategy) {
Strategy = Strategy.Strategy
} else {
logger.log2('info', `Loading node strategy module ${strategyModule}`)
Strategy = require(strategyModule)
if (provider.type === 'oauth' && Strategy.OAuth2Strategy) {
Strategy = Strategy.OAuth2Strategy
} else {
Strategy = Strategy.Strategy
}
logger.log2('verbose', 'Adding to list of known strategies')
passportStrategies.push({ id, Strategy })
}
const providerOptions = provider.options
const isSaml = strategyModule === 'passport-saml'
const verify = getVerifyFunction(provider)
// Create strategy
if (isSaml) {
// Turn off inResponseTo validation if the IDP is configured for IDP-initiated:
// "an IDP would never do both IDP initiated and SP initiated..."
if (global.iiconfig.authorizationParams.find(
authorizationParam => authorizationParam.provider === id)) {
providerOptions.validateInResponseTo = false
}
// Instantiate custom cache provider if required
if (providerOptions.validateInResponseTo) {
const f = R.anyPass([R.isNil, R.isEmpty])
const exp = providerOptions.requestIdExpirationPeriodMs / 1000
if (!f(providerOptions.redisCacheOptions)) {
providerOptions.cacheProvider = cacheProvider.get(
'redis', providerOptions.redisCacheOptions, exp
)
} else if (!f(providerOptions.memcachedCacheOptions)) {
providerOptions.cacheProvider = cacheProvider.get(
'memcached', providerOptions.memcachedCacheOptions, exp
)
}
}
const samlStrategy = new Strategy(providerOptions, verify)
passport.use(id, samlStrategy)
spMetadata.generate(provider, samlStrategy)
} else {
passport.use(id, new Strategy(providerOptions, verify))
}
}
function parseProp (value) {
try {
if (typeof value === 'string') {
value = JSON.parse(value)
}
} catch (err) {
// not an error. For datatypes other than string,
// the original parameter value is returned
}
return value
}
/**
* @TODO refactor ramda to native
*/
function fixDataTypes (providers) {
for (const provider of providers) {
// The subproperties of provider's options potentially come from the server as strings, they should
// be converted to other types if possible
let value = provider.options
if (misc.isObject(value)) {
R.forEach((key) => {
value[key] = parseProp(value[key])
}, R.keys(value))
} else {
logger.log2(
'warn', `Object expected for property options, found ${JSON.stringify(value)}`
)
value = {}
}
provider.options = value
// Tries to convert passportAuthnParams to a dictionary, otherwise {} is left
value = parseProp(provider.passportAuthnParams)
if (!misc.isObject(value)) {
// log message only if passportAuthnParams is not absent
if (!R.isNil(value)) {
logger.log2(
'warn', `Parsable object expected for property passportAuthnParams, found ${JSON.stringify(value)}`
)
}
value = {}
}
provider.passportAuthnParams = value
}
}
function mergeProperty (strategyId, obj, property) {
const extraParams = extraPassportParams.get(strategyId, property)
return R.mergeLeft(obj[property], extraParams)
}
function fillMissingData (providers) {
const paramsToFill = ['passportAuthnParams', 'options']
// eslint-disable-next-line no-return-assign
R.forEach(provider => R.forEach(prop => provider[prop] = mergeProperty(
provider.passportStrategyId, provider, prop), paramsToFill), providers)
for (const provider of providers) {
const options = provider.options
const strategyId = provider.passportStrategyId
const isSaml = strategyId === 'passport-saml'
const callbackUrl = R.defaultTo(options.callbackUrl, options.callbackURL)
const prefix = global.config.serverURI + '/passport/auth'
if (isSaml) {
// Different casing in saml
options.callbackUrl = R.defaultTo(`${prefix}/saml/${provider.id}/callback`, callbackUrl)
} else {
options.callbackURL = R.defaultTo(`${prefix}/${provider.id}/callback`, callbackUrl)
// Some passport strategies expect consumer* instead of client*
options.consumerKey = options.clientID
options.consumerSecret = options.clientSecret
// Allow state validation in passport-oauth2 based strategies
options.state = true
}
// Strategies with "special" treatments
if (strategyId.indexOf('passport-apple') >= 0 && options.key) {
// Smells like apple...
try {
// @TODO: we have to make the UI fields multiline so they can paste the contents and avoid this
options.key = require('fs').readFileSync(options.key, 'utf8')
} catch (e) {
logger.log2('warn', `There was a problem reading file ${options.key}. Ensure the file exists and is readable`)
logger.log2('error', e.stack)
options.key = ''
}
}
// Fills verifyCallbackArity (number expected)
const value = extraPassportParams.get(strategyId, 'verifyCallbackArity')
let toadd
if (options.passReqToCallback) {
toadd = 1
} else {
toadd = 0
}
// In most passport strategies the verify callback has arity 4 except for saml
if (typeof value === 'number') {
provider.verifyCallbackArity = value
} else {
let arity
if (isSaml) {
arity = 2
} else {
arity = 4
}
provider.verifyCallbackArity = toadd + arity
}
}
}
/**
* Setup providers and sets global `providers`
* @param providers : Object containing providers (fetched from config endpoint)
* @TODO refactor function name to setupProviders
*/
function setup (providers) {
providers = R.defaultTo([], providers)
const hashConfig = misc.hash(providers)
if (hashConfig !== prevConfigHash) {
// Only makes recomputations if config data changed
logger.log2('info', 'Reconfiguring providers')
prevConfigHash = hashConfig
// Unuse all strategies before reconfiguring
R.forEach(s => passport.unuse(s), R.map(R.prop('id'), passportStrategies))
// "Fix" incoming data
fixDataTypes(providers)
fillMissingData(providers)
R.forEach(setupStrategy, providers)
// Needed for routes.js
global.providers = providers
}
}
module.exports = {
setup: setup,
applyMapping: applyMapping
}