UNPKG

9.63 kBJavaScriptView Raw
1/**
2 * Cas
3 */
4var url = require('url')
5var http = require('http')
6var https = require('https')
7var passport = require('passport')
8var jsdom = require('jsdom')
9
10// query parameter used to request a gateway SSO
11var gatewayParameter = 'useGateway=true'
12
13/**
14 * Creates an instance of `Strategy`.
15 */
16function Strategy (options, verify) {
17 if (typeof options === 'function') {
18 verify = options
19 options = {}
20 }
21 if (!verify) {
22 throw new Error('cas authentication strategy requires a verify function')
23 }
24
25 this.ssoBase = options.ssoBaseURL
26 this.pgtUrl = options.pgtURL
27 this.serverBaseURL = options.serverBaseURL
28 this.parsed = url.parse(this.ssoBase)
29 if (this.parsed.protocol === 'http:') {
30 this.client = http
31 } else {
32 this.client = https
33 }
34
35 passport.Strategy.call(this)
36
37 this.name = 'cas'
38 this._verify = verify
39}
40
41/**
42 * Authenticate request.
43 *
44 * @param req The request to authenticate.
45 */
46Strategy.prototype.authenticate = function (req) {
47 var origUrl = req.originalUrl
48 var ticket = req.query.ticket
49 var service = url.resolve(this.serverBaseURL, origUrl)
50
51 // check if gateway SSO requested, remove any
52 // gateway query parameter from URL
53 var serviceUrl = url.parse(service, true)
54 delete serviceUrl.search
55 service = stripGatewayAuthenticationParameter(serviceUrl)
56
57 if (!ticket) {
58 // Building the redirect url to the login server
59 var loginServerURL = url.parse(this.ssoBase + '/login', true)
60
61 // Adding the gateway parameter if requested
62 if (useGatewayAuthentication(req)) {
63 loginServerURL.query.gateway = true
64 }
65
66 // Adding the service parameter
67 loginServerURL.query.service = service
68
69 // Redirecting to the login server.
70 return this.redirect(url.format(loginServerURL))
71 }
72
73 // Formatting the service url and adding the nextUrl parameter after it's done due to double encoding
74 // Adding the service parameter
75 // Re-creates the original service URL.
76 // Remove search and ticket since they are not valid now
77 var baseServiceUrl = url.resolve(this.serverBaseURL, origUrl)
78 var tmpUrl = url.parse(baseServiceUrl, true)
79 delete tmpUrl.search
80 delete tmpUrl.query.ticket
81 var nextUrl = stripGatewayAuthenticationParameter(tmpUrl)
82 var validateService = nextUrl
83
84 var self = this
85
86 /*
87 * Verifies the user login add set error, fail or success depending on the result.
88 */
89 var verified = function (err, user, info) {
90 if (err) {
91 return self.error(err)
92 }
93 if (!user) {
94 return self.fail(info)
95 }
96 self.success(user, info)
97 }
98
99 /**
100 * Request the login server's /validate with the ticket and service parameters.
101 * The callback function handles the CAS server response.
102 * Read more at the "CAS protocol section 2.4.2": http://www.jasig.org/cas/protocol
103 *
104 * Response on ticket validation success:
105 * yes
106 * u1foobar
107 *
108 * Response on ticket validation failure:
109 * no
110 */
111 var get = this.client.get({
112 host: this.parsed.hostname,
113 port: this.parsed.port,
114 path: url.format({
115 pathname: '/serviceValidate',
116 query: {
117 ticket: ticket,
118 service: validateService,
119 pgtUrl: this.pgtUrl
120 }
121 })
122 }, function (response) {
123 response.setEncoding('utf8')
124 var body = ''
125
126 response.on('data', function (responseData) {
127 body += responseData
128 })
129
130 return response.on('end', function () {
131 var parsedResult = parseCasResponse(body, ticket)
132 return parsedResult
133 .then(function (validationResult) {
134 return self._verify(validationResult, verified)
135 })
136 .catch(function () {
137 return self.fail(new Error('The response from the server was bad'))
138 })
139 })
140 })
141
142 get.on('error', function (e) {
143 return self.fail(new Error(e))
144 })
145}
146
147/**
148 * Check if we are requested to perform a gateway signon, i.e. a check
149 */
150function useGatewayAuthentication (req) {
151 // can be set on request if via application supplied callback
152 if (req.useGateway === true) {
153 return true
154 }
155
156 // otherwise via query parameter
157 var origUrl = req.originalUrl
158 var useGateway = false
159 var idx = origUrl.indexOf(gatewayParameter)
160 if (idx >= 0) {
161 useGateway = true
162 }
163
164 return useGateway
165}
166
167/**
168 * If a gateway query parameter is added, remove it.
169 */
170function stripGatewayAuthenticationParameter (aUrl) {
171 if (aUrl.query && aUrl.query.useGateway) {
172 delete aUrl.query.useGateway
173 }
174 if (aUrl.query.nextUrl) {
175 var theNextUrl = decodeURIComponent(aUrl.query.nextUrl)
176 aUrl.query.nextUrl = decodeURIComponent(theNextUrl)
177 }
178 var theUrl = url.format(aUrl)
179
180 return theUrl
181}
182
183function parseCasResponse (casResponse, ticket) {
184 // Use jsdom to parse the XML repsonse.
185 // ( Note:
186 // It seems jsdom currently does not support XML namespaces.
187 // And node names here are case insensitive. Hence attribute
188 // names will also be case insensitive.
189 // )
190 return new Promise(function (resolve, reject) {
191 jsdom.env(casResponse, function (err, window) {
192 if (err) {
193 return reject(new Error('jsdom could not parse casResponse: ' + casResponse))
194 }
195
196 // Check for auth success
197 var elemSuccess = window.document.getElementsByTagName('cas:authenticationSuccess')[ 0 ]
198 if (elemSuccess) {
199 var elemUser = elemSuccess.getElementsByTagName('cas:user')[ 0 ]
200 if (!elemUser) {
201 // This should never happen
202 return reject(new Error('No username?'), false)
203 }
204
205 // Got username
206 var username = elemUser.textContent
207
208 // Look for optional proxy granting ticket
209 var pgtIOU
210 var elemPGT = elemSuccess.getElementsByTagName('cas:proxyGrantingTicket')[ 0 ]
211 if (elemPGT) {
212 pgtIOU = elemPGT.textContent
213 }
214
215 // Look for optional proxies
216 var proxies = []
217 var elemProxies = elemSuccess.getElementsByTagName('cas:proxies')
218 for (var i = 0; i < elemProxies.length; i++) {
219 var thisProxy = elemProxies[ i ].textContent.trim()
220 proxies.push(thisProxy)
221 }
222
223 // Look for optional attributes
224 var casResponseParsed = { status: true, user: username, pgtIou: pgtIOU, 'ticket': ticket, 'proxies': proxies }
225
226 return resolve(casResponseParsed)
227 } // end if auth success
228
229 // Check for correctly formatted auth failure message
230 var elemFailure = window.document.getElementsByTagName('cas:authenticationFailure')[ 0 ]
231 if (elemFailure) {
232 var code = elemFailure.getAttribute('code')
233 var message = 'Validation failed [' + code + ']: '
234 message += elemFailure.textContent
235 return reject(new Error(message), false)
236 }
237
238 // The casResponse was not in any expected format, error
239 return reject(new Error('Bad casResponse format. ' + casResponse))
240 })
241 })
242}
243
244/**
245 * Get a proxy ticket using a proxy granting ticket.
246 * @param casService - the base URL to the CAS server i.e. without path, e.g. https://login-r.referens.sys.kth.se
247 * @param pgtId - the proxy granting ticket to use
248 * @param targetService - the service for which the proxy ticket will be used (to validate the ticket you need to supply this service)
249 * @returns {Promise} - resolved to a proxy ticket
250 * @private
251 */
252function _getProxyTicket (casService, pgtId, targetService) {
253 return new Promise(function (resolve, reject) {
254 // setup the url to the CAS Server
255 var parsedSsoBase
256 if (typeof casService === 'object') {
257 var ssoBase = casService.ssoBaseURL
258 parsedSsoBase = url.parse(ssoBase)
259 } else if (typeof casService === 'string') {
260 parsedSsoBase = url.parse(casService)
261 }
262
263 var proxyUrl = {
264 protocol: 'https:',
265 hostname: parsedSsoBase.hostname,
266 port: parsedSsoBase.port,
267 path: url.format({
268 pathname: '/proxy',
269 query: {
270 'targetService': targetService,
271 'pgt': pgtId
272 }
273 })
274 }
275
276 // Query the CAS server
277 var req = https.get(proxyUrl, function (res) {
278 // Handle server errors
279 res.on('error', function (e) {
280 return reject(e)
281 })
282
283 // Read result
284 res.setEncoding('utf8')
285 var response = ''
286 res.on('data', function (chunk) {
287 response += chunk
288 if (response.length > 1e6) {
289 req.connection.destroy()
290 }
291 })
292
293 // Reponse done, finish processing
294 res.on('end', function () {
295 // Use jsdom to parse the XML response
296 jsdom.env(response, function (err, window) {
297 // ERROR - unparseable response
298 if (err) {
299 return reject(new Error('Could not parse response: ' + response))
300 }
301
302 // OK - Got the proxy ticket
303 var elemTicket = window.document.getElementsByTagName('cas:proxyTicket')[ 0 ]
304 if (elemTicket) {
305 var proxyTicket = elemTicket.textContent
306 return resolve(proxyTicket)
307 }
308
309 // ERROR - Got a proxy failure
310 var elemFailure = window.document.getElementsByTagName('cas:proxyFailure')[ 0 ]
311 if (elemFailure) {
312 var code = elemFailure.getAttribute('code')
313 var message = 'Proxy failure [' + code + ']: '
314 message += elemFailure.textContent
315 return reject(new Error(message))
316 }
317
318 // ERROR - Unexpected response
319 return reject(new Error('Bad response format: ' + response))
320 })
321 })
322 })
323 })
324}
325
326/**
327 * Expose `Strategy`.
328 */
329exports.Strategy = Strategy
330exports.getProxyTicket = _getProxyTicket