-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathsaml.test.js
214 lines (202 loc) · 7.88 KB
/
saml.test.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
//
// Copyright 2024 Perforce Software
//
import * as fs from 'node:fs'
import * as https from 'node:https'
import { assert } from 'chai'
import { after, before, describe, it } from 'mocha'
import { Builder, By, Capabilities, until } from 'selenium-webdriver'
import { Options } from 'selenium-webdriver/firefox.js'
import { getRequestId } from 'helix-auth-svc/test/helpers.js'
//
// Selenium docs: https://www.selenium.dev/documentation/webdriver/
//
// Take a screenshot:
//
// const imagestr = await driver.takeScreenshot()
// fs.writeFileSync('screenshot.png', imagestr, 'base64')
//
// Dumping the page source:
//
// const pageSource = await driver.getPageSource()
// console.info('pageSource:', pageSource)
//
describe('SAML authentication', function () {
let driver
let requestId
let loginUrl
before(function () {
if (process.env.UNIT_ONLY) {
this.skip()
} else {
// starting the web driver may take longer than mocha would prefer
this.timeout(30000)
const caps = Capabilities.firefox().setAcceptInsecureCerts(true)
// fyi, going headless makes firefox 10x slower
const opts = new Options().addArguments('--headless')
// For the SAML test, need to control the page flow explicitly so disable
// the form auto-submit code in the client. Without this, attempting to
// find and collect page elements will sometimes fail due to the
// auto-submit code causing a page transition.
opts.setPreference('javascript.enabled', false)
driver = new Builder()
.forBrowser('firefox')
.withCapabilities(caps)
.setFirefoxOptions(opts)
.build()
}
})
after(async function () {
if (process.env.UNIT_ONLY === undefined) {
this.timeout(30000)
await driver.quit()
}
})
it('should return a SAML request identifier', async function () {
requestId = await getRequestId('authen.doc', 443)
loginUrl = 'https://authen.doc/saml/login/' + requestId
})
it('should reject invalid SAML user credentials', async function () {
// opening the browser (especially headless) can take a long time
this.timeout(30000)
await driver.get(loginUrl)
// activate the "loading session" form to get the login screen; with
// JavaScript disabled, shibboleth requires the user to click a button
const submitButton = await driver.wait(until.elementLocated(By.css('input[type="submit"]')))
await submitButton.click()
// fill in the credentials on the login screen
const loginForm = await driver.wait(until.elementLocated(By.css('form')))
const usernameBox = await loginForm.findElement(By.name('j_username'))
usernameBox.sendKeys('jackson')
const passwordBox = await loginForm.findElement(By.name('j_password'))
passwordBox.sendKeys('password123')
// .submit() resulted in "WebDriverError: HTTP method not allowed"
// await passwordBox.submit()
const continueButton = await loginForm.findElement(By.name('_eventId_proceed'))
await continueButton.click()
// <p class="output-message output--error">The password you entered was incorrect.</p>
const errorElem = await driver.wait(until.elementLocated(
By.xpath('//p[contains(@class, "output--error")]')), 10000)
const errorText = await errorElem.getText()
assert.include(errorText, 'password you entered was incorrect')
})
it('should not return SAML login status yet', function (done) {
this.timeout(30000)
// This request requires client certificates for security purposes. The
// supertest module does not allow setting rejectUnauthorized, and as such
// Node.js rejects the self-signed certificate from a host that is not
// localhost.
const cert = fs.readFileSync('test/client.crt')
const key = fs.readFileSync('test/client.key')
const req = https.get({
hostname: 'authen.doc',
path: `/requests/status/${requestId}`,
rejectUnauthorized: false,
requestCert: false,
agent: false,
timeout: 15000,
key,
cert
}, (res) => {
assert.equal(res.statusCode, 200)
}).on('timeout', () => {
req.destroy()
done()
}).on('error', (err) => {
if (err.code !== 'ECONNRESET') {
done(err)
}
})
})
it('should return a new SAML request identifier', async function () {
// Start a fresh request because the earlier one is still pending on the
// server and the data is deleted from the cache in a race condition.
requestId = await getRequestId('authen.doc', 443)
loginUrl = 'https://authen.doc/saml/login/' + requestId
})
it('should authenticate via SAML identity provider', async function () {
this.timeout(30000)
await driver.get(loginUrl)
// no need for this the second time apparently
// const submitButton = await driver.wait(until.elementLocated(By.css('input[type="submit"]')))
// await submitButton.click()
// fill in the credentials on the login screen
const loginForm = await driver.wait(until.elementLocated(By.css('form')))
const usernameBox = await loginForm.findElement(By.name('j_username'))
usernameBox.sendKeys('jackson')
const passwordBox = await loginForm.findElement(By.name('j_password'))
passwordBox.sendKeys('Passw0rd!')
// .submit() resulted in "WebDriverError: HTTP method not allowed"
// await passwordBox.submit()
const continueButton = await loginForm.findElement(By.name('_eventId_proceed'))
await continueButton.click()
// wait for the SAML response submit form
const submitButton = await driver.wait(until.elementLocated(By.css('input[type="submit"]')))
await submitButton.click()
await driver.wait(until.urlContains('authen.doc'), 10000)
const subtitleH2 = await driver.wait(until.elementLocated(By.className('subtitle')))
const subtitleText = await subtitleH2.getText()
assert.equal(subtitleText, 'Login Successful')
})
it('should return SAML login status of user', function (done) {
this.timeout(30000)
const cert = fs.readFileSync('test/client.crt')
const key = fs.readFileSync('test/client.key')
https.get({
hostname: 'authen.doc',
path: `/requests/status/${requestId}`,
rejectUnauthorized: false,
requestCert: false,
agent: false,
key,
cert
}, (res) => {
assert.equal(res.statusCode, 200)
assert.match(res.headers['content-type'], /^application\/json/)
res.setEncoding('utf-8')
let data = ''
res.on('data', (chunk) => { data += chunk })
res.on('end', () => {
const json = JSON.parse(data)
assert.equal(json.nameID, 'saml.jackson@example.com')
assert.equal(json.nameIDFormat, 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')
assert.exists(json.sessionIndex)
done()
})
}).on('error', (err) => {
done(err)
})
})
it('should log out of SAML identity provider', async function () {
this.timeout(30000)
await driver.get('https://authen.doc/saml/logout')
// wait for the SAML response submit form
const submitButton = await driver.wait(until.elementLocated(By.css('input[type="submit"]')))
await submitButton.click()
const divElem = await driver.wait(until.elementLocated(
By.xpath('//section[contains(@class, "Site-content")]/div')))
const divText = await divElem.getText()
assert.include(divText, 'Logout successful')
})
it('should return valid SAML metadata', function (done) {
https.get({
hostname: 'authen.doc',
path: '/saml/metadata',
rejectUnauthorized: false,
requestCert: false,
agent: false
}, (res) => {
assert.equal(res.statusCode, 200)
assert.match(res.headers['content-type'], /^text\/xml/)
res.setEncoding('utf-8')
let data = ''
res.on('data', (chunk) => { data += chunk })
res.on('end', () => {
assert.include(data, '<AssertionConsumerService')
done()
})
}).on('error', (err) => {
done(err)
})
})
})