-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathresponse.js
128 lines (110 loc) · 4.59 KB
/
response.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
import xmldom from 'xmldom'
import xmlenc from 'xml-encryption'
import Debug from 'debug'
import { SUCCESS_STATUS, PROFILEATTRS } from './consts'
import { checkSamlSignature, selectXpath, formatPEM, NIAError } from './utils'
const debug = Debug('saml2')
export function getStatus (dom) {
var statusNode = selectXpath('//samlp:Status/samlp:StatusCode', dom, 1)
if (!statusNode) throw new NIAError('Status not found')
return statusNode.getAttribute('Value')
}
export function decryptAssertion (dom, privateKeys) {
const privateKey = formatPEM(privateKeys[0], 'PRIVATE KEY')
return new Promise((resolve, reject) => {
const encryptedAssertion = selectXpath('//saml:EncryptedAssertion', dom, 1)
if (!encryptedAssertion) {
return reject(new NIAError('Expected 1 EncryptedAssertion found'))
}
const encryptedData = selectXpath('//xenc:EncryptedData', encryptedAssertion, 1)
if (!encryptedData) {
return reject(new NIAError('No EncryptedData inside EncryptedAssertion found'))
}
xmlenc.decrypt(encryptedAssertion, { key: privateKey }, (err, result) => {
if (err) return reject(err)
resolve(result)
})
})
}
export function logoutPostAssert (options, body) {
const raw = Buffer.from(body.SAMLResponse || body.SAMLRequest, 'base64')
const samlResponse = (new xmldom.DOMParser()).parseFromString(raw.toString())
debug(samlResponse)
const status = getStatus(samlResponse)
if (status !== SUCCESS_STATUS) {
throw new NIAError('SAML Response not success!')
}
return parseResponseHeader(samlResponse, 'LogoutResponse')
}
export function loginPostAssert (options, body) {
const raw = Buffer.from(body.SAMLResponse || body.SAMLRequest, 'base64')
const samlResponse = (new xmldom.DOMParser()).parseFromString(raw.toString())
debug(samlResponse)
const response = {
header: parseResponseHeader(samlResponse, 'Response')
}
const status = getStatus(samlResponse)
if (status !== SUCCESS_STATUS) throw new NIAError('Status: ' + status)
return decryptAssertion(samlResponse, [options.private_key])
.then(decriptedXML => {
const decriptedDoc = new xmldom.DOMParser().parseFromString(decriptedXML)
const valid = checkSamlSignature(decriptedDoc, decriptedXML, options.certificates[0])
if (!valid) throw new NIAError('response signature not mathed')
checkAuthResponse(decriptedXML, options)
parseAssertionAttributes(decriptedDoc, response)
return response
})
}
export function parseResponseHeader (dom, tag) {
const response = selectXpath(`//samlp:${tag}`, dom, 1)
if (!response) throw new NIAError('No Response found')
const responseHeader = {
version: response.getAttribute('Version'),
destination: response.getAttribute('Destination'),
in_response_to: response.getAttribute('InResponseTo'),
id: response.getAttribute('ID')
}
return responseHeader
}
function parseComplexProfileAttr (val) {
const decoded = Buffer.from(val, 'base64')
const dom = (new xmldom.DOMParser()).parseFromString(decoded.toString())
const subAttrs = selectXpath('//*', dom)
return subAttrs.reduce((obj, i) => {
const val = selectXpath('string(node())', i)
if (val) obj[i.localName] = val
return obj
}, {})
}
const COMPLEX_ATTRS = [
PROFILEATTRS.CURRENT_ADDRESS,
PROFILEATTRS.CZMORIS_TR_ADRESA_ID
]
function parseAssertionAttributes (dom, response) {
response.NameID = selectXpath('string(//saml:Subject/saml:NameID)', dom, 1)
response.SessionIndex = selectXpath('//saml:AuthnStatement', dom, 1)
.getAttribute('SessionIndex')
response.LoA = selectXpath('string(//saml:AuthnContextClassRef)', dom, 1)
const vals = selectXpath('//saml:AttributeStatement//saml:Attribute', dom)
response.user = {}
vals.map(i => {
const name = i.getAttribute('Name')
const frendlyName = i.getAttribute('FriendlyName')
const val = selectXpath('string(saml:AttributeValue)', i)
response.user[frendlyName] = COMPLEX_ATTRS.indexOf(name) < 0
? val : parseComplexProfileAttr(val)
})
}
function checkAuthResponse (dom, opts) {
const conditions = selectXpath('//saml:Conditions', dom, 1)
if (!conditions) return
const notBefore = Date.parse(conditions.getAttribute('NotBefore'))
const notAfter = Date.parse(conditions.getAttribute('NotOnOrAfter'))
const now = Date.now()
if (now > notBefore) throw new NIAError('SAML Response is not yet valid')
if (now < notAfter) throw new NIAError('SAML Response is no longer valid')
const audience = selectXpath('string(//saml:AudienceRestriction/Audience)', dom, 1)
if (audience !== opts.audience) {
throw new NIAError('Response is not valid for this audience')
}
}