Skip to content

Commit d45f471

Browse files
author
anderson.martins
committed
Support the ability to parse IdP XML metadata (remote url or file) and
be able to inject the data obtained on the settings.
1 parent fa1df60 commit d45f471

File tree

8 files changed

+948
-3
lines changed

8 files changed

+948
-3
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
package com.onelogin.saml2.settings;
2+
3+
import java.io.InputStream;
4+
import java.net.URL;
5+
import java.util.LinkedHashMap;
6+
import java.util.Map;
7+
8+
import javax.xml.xpath.XPathException;
9+
import javax.xml.xpath.XPathExpressionException;
10+
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
import org.w3c.dom.Document;
14+
import org.w3c.dom.Node;
15+
import org.w3c.dom.NodeList;
16+
import org.xml.sax.InputSource;
17+
18+
import com.onelogin.saml2.exception.Error;
19+
import com.onelogin.saml2.util.Constants;
20+
import com.onelogin.saml2.util.Util;
21+
22+
/**
23+
* IdPMetadataParser class of OneLogin's Java Toolkit.
24+
*
25+
* A class that implements the settings parser from IdP Metadata
26+
*
27+
*/
28+
public class IdPMetadataParser {
29+
30+
/**
31+
* Private property to construct a logger for this class.
32+
*/
33+
private static final Logger LOGGER = LoggerFactory.getLogger(IdPMetadataParser.class);
34+
35+
/**
36+
* Get IdP Metadata Info from XML Document
37+
*
38+
* @param xmlDocument
39+
* XML document hat contains IdP metadata
40+
* @param entityId
41+
* Entity Id of the desired IdP, if no entity Id is provided and the XML metadata contains more than one IDPSSODescriptor, the first is returned
42+
* @param desiredNameIdFormat
43+
* If available on IdP metadata, use that nameIdFormat
44+
* @param desiredSSOBinding
45+
* Parse specific binding SSO endpoint.
46+
* @param desiredSLOBinding
47+
* Parse specific binding SLO endpoint.
48+
*
49+
* @return Mapped values with metadata info in Saml2Settings format
50+
* @throws XPathExpressionException
51+
*/
52+
public static Map<String, Object> parseXML(Document xmlDocument, String entityId, String desiredNameIdFormat, String desiredSSOBinding, String desiredSLOBinding) throws XPathException {
53+
Map<String, Object> metadataInfo = new LinkedHashMap<>();
54+
55+
try {
56+
String customIdPStr = "";
57+
if (entityId != null && !entityId.isEmpty()) {
58+
customIdPStr = "[@entityID=\"" + entityId + "\"]";
59+
}
60+
61+
String idpDescryptorXPath = "//md:EntityDescriptor" + customIdPStr + "/md:IDPSSODescriptor";
62+
63+
NodeList idpDescriptorNodes = Util.query(xmlDocument, idpDescryptorXPath);
64+
65+
if (idpDescriptorNodes.getLength() > 0) {
66+
67+
Node idpDescriptorNode = idpDescriptorNodes.item(0);
68+
if (entityId == null || entityId.isEmpty()) {
69+
Node entityIDNode = idpDescriptorNode.getParentNode().getAttributes().getNamedItem("entityID");
70+
if (entityIDNode != null) {
71+
entityId = entityIDNode.getNodeValue();
72+
}
73+
}
74+
75+
if (entityId != null && !entityId.isEmpty()) {
76+
metadataInfo.put(SettingsBuilder.IDP_ENTITYID_PROPERTY_KEY, entityId);
77+
}
78+
79+
NodeList ssoNodes = Util.query(xmlDocument, "./md:SingleSignOnService[@Binding=\"" + desiredSSOBinding + "\"]", idpDescriptorNode);
80+
if (ssoNodes.getLength() < 1) {
81+
ssoNodes = Util.query(xmlDocument, "./md:SingleSignOnService", idpDescriptorNode);
82+
}
83+
if (ssoNodes.getLength() > 0) {
84+
metadataInfo.put(SettingsBuilder.IDP_SINGLE_SIGN_ON_SERVICE_URL_PROPERTY_KEY, ssoNodes.item(0).getAttributes().getNamedItem("Location").getNodeValue());
85+
metadataInfo.put(SettingsBuilder.IDP_SINGLE_SIGN_ON_SERVICE_BINDING_PROPERTY_KEY, ssoNodes.item(0).getAttributes().getNamedItem("Binding").getNodeValue());
86+
}
87+
88+
NodeList sloNodes = Util.query(xmlDocument, "./md:SingleLogoutService[@Binding=\"" + desiredSLOBinding + "\"]", idpDescriptorNode);
89+
if (sloNodes.getLength() < 1) {
90+
sloNodes = Util.query(xmlDocument, "./md:SingleLogoutService", idpDescriptorNode);
91+
}
92+
if (sloNodes.getLength() > 0) {
93+
metadataInfo.put(SettingsBuilder.IDP_SINGLE_LOGOUT_SERVICE_URL_PROPERTY_KEY, sloNodes.item(0).getAttributes().getNamedItem("Location").getNodeValue());
94+
metadataInfo.put(SettingsBuilder.IDP_SINGLE_LOGOUT_SERVICE_BINDING_PROPERTY_KEY, sloNodes.item(0).getAttributes().getNamedItem("Binding").getNodeValue());
95+
}
96+
97+
NodeList keyDescriptorCertSigningNodes = Util.query(xmlDocument, "./md:KeyDescriptor[not(contains(@use, \"encryption\"))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
98+
idpDescriptorNode);
99+
100+
NodeList keyDescriptorCertEncryptionNodes = Util.query(xmlDocument, "./md:KeyDescriptor[not(contains(@use, \"signing\"))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
101+
idpDescriptorNode);
102+
103+
if (keyDescriptorCertSigningNodes.getLength() > 0 || keyDescriptorCertEncryptionNodes.getLength() > 0) {
104+
105+
boolean hasEncryptionCert = keyDescriptorCertEncryptionNodes.getLength() > 0;
106+
String encryptionCert = null;
107+
108+
if (hasEncryptionCert) {
109+
encryptionCert = keyDescriptorCertEncryptionNodes.item(0).getTextContent();
110+
metadataInfo.put(SettingsBuilder.IDP_X509CERT_PROPERTY_KEY, encryptionCert);
111+
}
112+
113+
if (keyDescriptorCertSigningNodes.getLength() > 0) {
114+
int index = 0;
115+
for (int i = 0; i < keyDescriptorCertSigningNodes.getLength(); i++) {
116+
String signingCert = keyDescriptorCertSigningNodes.item(i).getTextContent();
117+
if (i == 0 && !hasEncryptionCert) {
118+
metadataInfo.put(SettingsBuilder.IDP_X509CERT_PROPERTY_KEY, signingCert);
119+
} else if (!hasEncryptionCert || !encryptionCert.equals(signingCert)) {
120+
metadataInfo.put(SettingsBuilder.IDP_X509CERTMULTI_PROPERTY_KEY + "." + (index++), signingCert);
121+
}
122+
}
123+
}
124+
}
125+
126+
NodeList nameIdFormatNodes = Util.query(xmlDocument, "./md:NameIDFormat", idpDescriptorNode);
127+
for (int i = 0; i < nameIdFormatNodes.getLength(); i++) {
128+
String nameIdFormat = nameIdFormatNodes.item(i).getTextContent();
129+
if (nameIdFormat != null && (desiredNameIdFormat == null || desiredNameIdFormat.equals(nameIdFormat))) {
130+
metadataInfo.put(SettingsBuilder.SP_NAMEIDFORMAT_PROPERTY_KEY, nameIdFormat);
131+
break;
132+
}
133+
}
134+
}
135+
} catch (XPathException e) {
136+
String errorMsg = "Error parsing metadata. " + e.getMessage();
137+
LOGGER.error(errorMsg, e);
138+
throw e;
139+
}
140+
141+
return metadataInfo;
142+
}
143+
144+
/**
145+
* Get IdP Metadata Info from XML Document
146+
*
147+
* @param xmlDocument
148+
* XML document hat contains IdP metadata
149+
* @param entityId
150+
* Entity Id of the desired IdP, if no entity Id is provided and the XML metadata contains more than one IDPSSODescriptor, the first is returned
151+
*
152+
* @return Mapped values with metadata info in Saml2Settings format
153+
* @throws XPathException
154+
*/
155+
public static Map<String, Object> parseXML(Document xmlDocument, String entityId) throws XPathException {
156+
return parseXML(xmlDocument, entityId, null, Constants.BINDING_HTTP_REDIRECT, Constants.BINDING_HTTP_REDIRECT);
157+
}
158+
159+
/**
160+
* Get IdP Metadata Info from XML Document
161+
*
162+
* @param xmlDocument
163+
* XML document hat contains IdP metadata
164+
*
165+
* @return Mapped values with metadata info in Saml2Settings format
166+
* @throws XPathException
167+
*/
168+
public static Map<String, Object> parseXML(Document xmlDocument) throws XPathException {
169+
return parseXML(xmlDocument, null);
170+
}
171+
172+
/**
173+
* Get IdP Metadata Info from XML file
174+
*
175+
* @param xmlDocument
176+
* XML document hat contains IdP metadata
177+
* @param entityId
178+
* Entity Id of the desired IdP, if no entity Id is provided and the XML metadata contains more than one IDPSSODescriptor, the first is returned
179+
* @param desiredNameIdFormat
180+
* If available on IdP metadata, use that nameIdFormat
181+
* @param desiredSSOBinding
182+
* Parse specific binding SSO endpoint.
183+
* @param desiredSLOBinding
184+
* Parse specific binding SLO endpoint.
185+
*
186+
* @return Mapped values with metadata info in Saml2Settings format
187+
* @throws Exception
188+
*/
189+
public static Map<String, Object> parseFileXML(String xmlFileName, String entityId, String desiredNameIdFormat, String desiredSSOBinding, String desiredSLOBinding) throws Exception {
190+
ClassLoader classLoader = IdPMetadataParser.class.getClassLoader();
191+
try (InputStream inputStream = classLoader.getResourceAsStream(xmlFileName)) {
192+
if (inputStream != null) {
193+
Document xmlDocument = Util.parseXML(new InputSource(inputStream));
194+
return parseXML(xmlDocument, entityId, desiredNameIdFormat, desiredSSOBinding, desiredSLOBinding);
195+
} else {
196+
throw new Exception("XML file '" + xmlFileName + "' not found in the classpath");
197+
}
198+
} catch (Exception e) {
199+
String errorMsg = "XML file'" + xmlFileName + "' cannot be loaded." + e.getMessage();
200+
LOGGER.error(errorMsg, e);
201+
throw new Error(errorMsg, Error.SETTINGS_FILE_NOT_FOUND);
202+
}
203+
}
204+
205+
/**
206+
* Get IdP Metadata Info from XML file
207+
*
208+
* @param xmlDocument
209+
* XML document hat contains IdP metadata
210+
* @param entityId
211+
* Entity Id of the desired IdP, if no entity Id is provided and the XML metadata contains more than one IDPSSODescriptor, the first is returned
212+
*
213+
* @return Mapped values with metadata info in Saml2Settings format
214+
* @throws Exception
215+
*/
216+
public static Map<String, Object> parseFileXML(String xmlFileName, String entityId) throws Exception {
217+
return parseFileXML(xmlFileName, entityId, null, Constants.BINDING_HTTP_REDIRECT, Constants.BINDING_HTTP_REDIRECT);
218+
}
219+
220+
/**
221+
* Get IdP Metadata Info from XML file
222+
*
223+
* @param xmlDocument
224+
* XML document hat contains IdP metadata
225+
*
226+
* @return Mapped values with metadata info in Saml2Settings format
227+
* @throws Exception
228+
*/
229+
public static Map<String, Object> parseFileXML(String xmlFileName) throws Exception {
230+
return parseFileXML(xmlFileName, null);
231+
}
232+
233+
/**
234+
* Get IdP Metadata Info from XML file
235+
*
236+
* @param xmlDocument
237+
* XML document hat contains IdP metadata
238+
* @param entityId
239+
* Entity Id of the desired IdP, if no entity Id is provided and the XML metadata contains more than one IDPSSODescriptor, the first is returned
240+
* @param desiredNameIdFormat
241+
* If available on IdP metadata, use that nameIdFormat
242+
* @param desiredSSOBinding
243+
* Parse specific binding SSO endpoint.
244+
* @param desiredSLOBinding
245+
* Parse specific binding SLO endpoint.
246+
*
247+
* @return Mapped values with metadata info in Saml2Settings format
248+
* @throws Exception
249+
*/
250+
public static Map<String, Object> parseRemoteXML(URL xmlURL, String entityId, String desiredNameIdFormat, String desiredSSOBinding, String desiredSLOBinding) throws Exception {
251+
Document xmlDocument = Util.parseXML(new InputSource(xmlURL.openStream()));
252+
return parseXML(xmlDocument, entityId, desiredNameIdFormat, desiredSSOBinding, desiredSLOBinding);
253+
}
254+
255+
/**
256+
* Get IdP Metadata Info from XML file
257+
*
258+
* @param xmlDocument
259+
* XML document hat contains IdP metadata
260+
* @param entityId
261+
* Entity Id of the desired IdP, if no entity Id is provided and the XML metadata contains more than one IDPSSODescriptor, the first is returned
262+
*
263+
* @return Mapped values with metadata info in Saml2Settings format
264+
* @throws Exception
265+
*/
266+
public static Map<String, Object> parseRemoteXML(URL xmlURL, String entityId) throws Exception {
267+
return parseRemoteXML(xmlURL, entityId, null, Constants.BINDING_HTTP_REDIRECT, Constants.BINDING_HTTP_REDIRECT);
268+
}
269+
270+
/**
271+
* Get IdP Metadata Info from XML file
272+
*
273+
* @param xmlDocument
274+
* XML document hat contains IdP metadata
275+
*
276+
* @return Mapped values with metadata info in Saml2Settings format
277+
* @throws Exception
278+
*/
279+
public static Map<String, Object> parseRemoteXML(URL xmlURL) throws Exception {
280+
return parseRemoteXML(xmlURL, null);
281+
}
282+
283+
/**
284+
* Inject metadata info into Saml2Settings
285+
*
286+
* @param settings
287+
* the Saml2Settings object
288+
* @param metadataInfo
289+
* mapped values with metadata info in Saml2Settings format
290+
*
291+
* @return the Saml2Settings object with metadata info settings loaded
292+
*/
293+
public static Saml2Settings injectIntoSettings(Saml2Settings settings, Map<String, Object> metadataInfo) {
294+
295+
SettingsBuilder settingsBuilder = new SettingsBuilder().fromValues(metadataInfo);
296+
settingsBuilder.build(settings);
297+
return settings;
298+
}
299+
300+
}

core/src/main/java/com/onelogin/saml2/settings/SettingsBuilder.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,22 @@ public SettingsBuilder fromValues(Map<String, Object> samlData) {
173173
*
174174
*/
175175
public Saml2Settings build() {
176+
return build(new Saml2Settings());
177+
}
178+
179+
/**
180+
* Builds the Saml2Settings object. Read the Properties object and set all the SAML settings
181+
*
182+
* @param saml2Setting
183+
* an existing Saml2Settings
184+
*
185+
* @return the Saml2Settings object with all the SAML settings loaded
186+
*
187+
*/
188+
public Saml2Settings build(Saml2Settings saml2Setting) {
189+
190+
this.saml2Setting = saml2Setting;
176191

177-
saml2Setting = new Saml2Settings();
178-
179192
Boolean strict = loadBooleanProperty(STRICT_PROPERTY_KEY);
180193
if (strict != null)
181194
saml2Setting.setStrict(strict);

core/src/main/java/com/onelogin/saml2/util/Util.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,22 @@ public static boolean validateXML(Document xmlDocument, URL schemaUrl) {
292292
* @throws IOException
293293
*/
294294
public static Document convertStringToDocument(String xmlStr) throws ParserConfigurationException, SAXException, IOException {
295+
return parseXML(new InputSource(new StringReader(xmlStr)));
296+
}
297+
298+
/**
299+
* Parse an XML from input source to a Document object
300+
*
301+
* @param xmlStr
302+
* The XML string which should be converted
303+
*
304+
* @return the Document object
305+
*
306+
* @throws ParserConfigurationException
307+
* @throws SAXException
308+
* @throws IOException
309+
*/
310+
public static Document parseXML(InputSource inputSource) throws ParserConfigurationException, SAXException, IOException {
295311
DocumentBuilderFactory docfactory = DocumentBuilderFactory.newInstance();
296312
docfactory.setNamespaceAware(true);
297313

@@ -329,7 +345,7 @@ public static Document convertStringToDocument(String xmlStr) throws ParserConfi
329345
} catch (Throwable e) {}
330346

331347
DocumentBuilder builder = docfactory.newDocumentBuilder();
332-
Document doc = builder.parse(new InputSource(new StringReader(xmlStr)));
348+
Document doc = builder.parse(inputSource);
333349

334350
// Loop through the doc and tag every element with an ID attribute
335351
// as an XML ID node.

0 commit comments

Comments
 (0)