source: ccr/trunk/nhin-vista/projects/NHINC/Current/Product/Production/Gateway/NhincSamlTokenExtractionLib/src/gov/hhs/fha/nhinc/saml/extraction/SamlTokenExtractor.java@ 507

Last change on this file since 507 was 507, checked in by George Lilly, 15 years ago

NHIN gateway and adaptor for use on linux with VistA EHR and RPMS

File size: 27.7 KB
Line 
1/*
2 * To change this template, choose Tools | Templates
3 * and open the template in the editor.
4 */
5package gov.hhs.fha.nhinc.saml.extraction;
6
7import com.sun.org.apache.xerces.internal.dom.ElementNSImpl;
8import com.sun.xml.wss.SubjectAccessor;
9import com.sun.xml.wss.XWSSecurityException;
10import com.sun.xml.wss.impl.XWSSecurityRuntimeException;
11import com.sun.xml.wss.saml.SAMLAssertionFactory;
12import com.sun.xml.wss.saml.SAMLException;
13import com.sun.xml.wss.saml.internal.saml20.jaxb20.AttributeStatementType;
14import com.sun.xml.wss.saml.internal.saml20.jaxb20.AttributeType;
15import com.sun.xml.wss.saml.internal.saml20.jaxb20.ConditionsType;
16import com.sun.xml.wss.saml.internal.saml20.jaxb20.EvidenceType;
17import com.sun.xml.wss.saml.internal.saml20.jaxb20.AuthnStatementType;
18import com.sun.xml.wss.saml.internal.saml20.jaxb20.AuthzDecisionStatementType;
19import com.sun.xml.wss.saml.internal.saml20.jaxb20.NameIDType;
20import gov.hhs.fha.nhinc.common.nhinccommon.AssertionType;
21
22import gov.hhs.fha.nhinc.common.nhinccommon.CeType;
23import gov.hhs.fha.nhinc.common.nhinccommon.HomeCommunityType;
24import gov.hhs.fha.nhinc.common.nhinccommon.PersonNameType;
25import gov.hhs.fha.nhinc.common.nhinccommon.UserType;
26import gov.hhs.fha.nhinc.nhinclib.NullChecker;
27import java.text.SimpleDateFormat;
28import java.util.ArrayList;
29import java.util.List;
30import java.util.Set;
31import javax.security.auth.Subject;
32import javax.security.auth.x500.X500Principal;
33import javax.xml.bind.JAXBElement;
34import javax.xml.datatype.XMLGregorianCalendar;
35import javax.xml.stream.XMLStreamReader;
36import javax.xml.ws.WebServiceContext;
37import org.apache.commons.logging.Log;
38import org.apache.commons.logging.LogFactory;
39import org.w3c.dom.Node;
40import org.w3c.dom.NamedNodeMap;
41import org.w3c.dom.NodeList;
42import sun.security.x509.AVA;
43import sun.security.x509.X500Name;
44
45/**
46 *
47 * @author Jon Hoppesch
48 */
49public class SamlTokenExtractor {
50
51 private static Log log = LogFactory.getLog(SamlTokenExtractor.class);
52 private static final String X509_FORMAT = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName";
53 private static final String USER_ROLE_ID = "UserRole";
54 private static final String PURPOSE_ROLE_ID = "PurposeForUse";
55 private static final String USERNAME_ID = "UserName";
56 private static final String USERORG_ID = "UserOrganization";
57 private static final String CONTENTREF_ID = "ContentReference";
58 private static final String CONTENT_ID = "Content";
59 private static final String CONTENTTYPE_ID = "ContentType";
60 private static final String CE_CODE_ID = "code";
61 private static final String CE_CODESYS_ID = "codesystem";
62 private static final String CE_CODESYSNAME_ID = "codeSystemName";
63 private static final String CE_DISPLAYNAME_ID = "displayName";
64
65 public static AssertionType GetAssertion(WebServiceContext context) {
66 log.debug("Entering SamlTokenExtractor.GetAssertion...");
67
68 AssertionType assertion = new AssertionType();
69
70 try {
71 Subject subj = SubjectAccessor.getRequesterSubject(context);
72 Set<Object> set = subj.getPublicCredentials();
73 for (Object obj : set) {
74 if (obj instanceof XMLStreamReader) {
75 XMLStreamReader reader = (XMLStreamReader) obj;
76 assertion = SamlTokenExtractor.CreateAssertion(reader);
77 }
78 }
79 } catch (XWSSecurityException ex) {
80 log.error("Error extracting the SAML Token: " + ex);
81 throw new XWSSecurityRuntimeException(ex);
82 }
83
84 log.debug("Exiting SamlTokenExtractor.GetAssertion...");
85 return assertion;
86 }
87
88 public static AssertionType CreateAssertion(XMLStreamReader reader) {
89 log.debug("Entering SamlTokenExtractor.CreateAssertion...");
90
91 AssertionType assertion = new AssertionType();
92 try {
93 SAMLAssertionFactory factory = SAMLAssertionFactory.newInstance(SAMLAssertionFactory.SAML2_0);
94 com.sun.xml.wss.saml.Assertion assertIn = factory.createAssertion(reader);
95
96 if (assertIn instanceof com.sun.xml.wss.saml.internal.saml20.jaxb20.AssertionType) {
97 com.sun.xml.wss.saml.internal.saml20.jaxb20.AssertionType assertType = (com.sun.xml.wss.saml.internal.saml20.jaxb20.AssertionType) assertIn;
98 extractSubject(assertType, assertion);
99 }
100
101 List statements = assertIn.getStatements();
102 if (statements != null && !statements.isEmpty()) {
103 for (int idx = 0; idx < statements.size(); idx++) {
104 if (statements.get(idx) instanceof AttributeStatementType) {
105 AttributeStatementType statement = (AttributeStatementType) statements.get(idx);
106 extractAttributeInfo(statement, assertion);
107 } else if (statements.get(idx) instanceof AuthzDecisionStatementType) {
108 AuthzDecisionStatementType statement = (AuthzDecisionStatementType) statements.get(idx);
109 extractDecisionInfo(statement, assertion);
110 } else if (statements.get(idx) instanceof AuthnStatementType) {
111 // Currently nothing done for AuthnStatement
112 } else {
113 log.warn("Unknown statement type: " + statements.get(idx));
114 }
115 }
116 } else {
117 log.error("There were no statements specified in the SAML token.");
118 assertion = null;
119 }
120 } catch (SAMLException ex) {
121 log.error("SAML Exception Thrown: " + ex.getMessage());
122 assertion = null;
123 ;
124 } catch (XWSSecurityException ex) {
125 log.error("Security Exception Thrown: " + ex.getMessage());
126 assertion = null;
127 }
128
129 log.debug("Exiting SamlTokenExtractor.CreateAssertion");
130 return assertion;
131 }
132
133 /**
134 * The Subject element contains the user identification information. It is
135 * expected to follow an X509 format, but in the case that this is not
136 * proven out it will save the entire contents as being the userid.
137 * @param assertType The saml assertion type being read from
138 * @param assertOut The Assertion element being written to
139 */
140 private static void extractSubject(com.sun.xml.wss.saml.internal.saml20.jaxb20.AssertionType assertType, AssertionType assertOut) {
141 log.debug("Entering SamlTokenExtractor.extractSubject...");
142
143 UserType userInfo = new UserType();
144
145 if (assertType.getSubject() != null) {
146 List<JAXBElement<?>> contents = assertType.getSubject().getContent();
147 if (contents != null && !contents.isEmpty()) {
148 for (JAXBElement jaxElem : contents) {
149 if (jaxElem.getValue() instanceof NameIDType) {
150 NameIDType nameId = (NameIDType) jaxElem.getValue();
151 String format = nameId.getFormat();
152 String nameVal = nameId.getValue();
153
154 // For X509 format the user identifier is extracted, for others content is taken as is
155 String userIdentifier = nameVal;
156 if (NullChecker.isNotNullish(format) && NullChecker.isNotNullish(nameVal)) {
157 if (format.trim().equals(X509_FORMAT)) {
158 String extractedUID = extract509(nameVal);
159 if (NullChecker.isNotNullish(extractedUID)) {
160 userIdentifier = extractedUID;
161 } else {
162 log.warn("X509 Formatted user identifier cannot be extracted");
163 }
164 }
165 } else {
166 log.warn("Subject's NameId has an invalid format");
167 }
168 log.info("Setting UserName to: " + userIdentifier);
169 userInfo.setUserName(userIdentifier);
170 assertOut.setUserInfo(userInfo);
171 }
172 }
173 } else {
174 log.warn("Expected Subject information is missing.");
175 }
176 } else {
177 log.warn("Subject element is missing.");
178 }
179
180 log.debug("Exiting SamlTokenExtractor.extractSubject...");
181 }
182
183 /**
184 * Extracts the value of the UID object identifier of the X509 formatted
185 * content. This method uses Sun proprietary classes to determine if the
186 * X509 is properly formed and to extract the value of the UID. The current
187 * specification for the string representation of a distinguished name is
188 * defined in <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>.
189 * @param in509 The X509 formatted string
190 * @return The extracted userid value, null if not defined.
191 */
192 private static String extract509(String in509) {
193 log.debug("Entering SamlTokenExtractor.extract509...");
194
195 String userVal = null;
196
197 if (NullChecker.isNotNullish(in509)) {
198 try {
199 X500Principal prin = new X500Principal(in509);
200 X500Name name500 = X500Name.asX500Name(prin);
201 for (AVA ava : name500.allAvas()) {
202 if (X500Name.userid_oid == ava.getObjectIdentifier()) {
203 userVal = ava.getValueString();
204 } else {
205 log.warn("Construction of user identifier does not use: " + ava.toString());
206 }
207 }
208 } catch (IllegalArgumentException iae) {
209 log.error("X509 NameId is not properly formed: " + iae.getMessage());
210 }
211 }
212
213 log.debug("Exiting SamlTokenExtractor.extract509...");
214 return userVal;
215 }
216
217 /**
218 * This method is responsible to extract the information from both the
219 * AttributeStatements as found in the main Assertion element as well as the
220 * AttributeStatements found in the Evidence element. The permitted names
221 * of the Attributes in the Assertion element are: UserRole, PurposeForUse,
222 * UserName, UserOrganization. The permitted names of the Attributes in the
223 * Evidence element are: ContentReference, ContentType, and Content.
224 * @param statement The attribute statement to be extracted
225 * @param assertOut The Assertion element being written to
226 */
227 private static void extractAttributeInfo(AttributeStatementType statement, AssertionType assertOut) {
228 log.debug("Entering SamlTokenExtractor.extractAttributeInfo...");
229
230 List attribs = statement.getAttributeOrEncryptedAttribute();
231
232 if (attribs != null && !attribs.isEmpty()) {
233 for (int idx = 0; idx < attribs.size(); idx++) {
234 if (attribs.get(idx) instanceof AttributeType) {
235 AttributeType attrib = (AttributeType) attribs.get(idx);
236 String nameAttr = attrib.getName();
237 if (nameAttr != null) {
238 if (nameAttr.equals(USER_ROLE_ID)) {
239 assertOut.getUserInfo().setRoleCoded(parseRole(attrib, USER_ROLE_ID));
240 } else if (nameAttr.equals(PURPOSE_ROLE_ID)) {
241 assertOut.setPurposeOfDisclosureCoded(parseRole(attrib, PURPOSE_ROLE_ID));
242 } else if (nameAttr.equals(USERNAME_ID)) {
243 extractNameParts(attrib, assertOut);
244 } else if (nameAttr.equals(USERORG_ID) ||
245 nameAttr.equals(CONTENTREF_ID) ||
246 nameAttr.equals(CONTENT_ID)) {
247 List attrVals = attrib.getAttributeValue();
248 if (attrVals != null && !attrVals.isEmpty()) {
249 if (nameAttr.equals(CONTENT_ID)) {
250 for (int idxVal = 0; idxVal < attrVals.size(); idxVal++) {
251 Object formVal = attrVals.get(idxVal);
252
253 if (formVal instanceof byte[]) {
254 byte[] rawForm = (byte[]) attrVals.get(idxVal);
255 log.info("Setting Claim Form to: " + rawForm.toString());
256 assertOut.setClaimFormRaw(rawForm);
257 } else {
258 log.error(nameAttr + " Attribute is not recognized as base64 binary");
259 }
260 }
261 } else {
262 StringBuffer strBuf = new StringBuffer();
263 for (int idxVal = 0; idxVal < attrVals.size(); idxVal++) {
264 strBuf.append(attrVals.get(idxVal) + " ");
265 }
266 if (nameAttr.equals(CONTENTREF_ID)) {
267 log.info("Setting ContentReference to: " + strBuf.toString().trim());
268 assertOut.setClaimFormRef(strBuf.toString().trim());
269 } else if (nameAttr.equals(USERORG_ID)) {
270 log.info("Setting UserOrganization to: " + strBuf.toString().trim());
271 HomeCommunityType homeCommunity = new HomeCommunityType();
272 homeCommunity.setName(strBuf.toString().trim());
273 assertOut.getUserInfo().setOrg(homeCommunity);
274 }
275 }
276 } else {
277 log.warn("No values are provided for Attribute: " + nameAttr);
278 }
279 } else if (nameAttr.equals(CONTENTTYPE_ID)) {
280 log.info(nameAttr + " is set to default 'application\\pdf'.");
281 } else {
282 log.warn("Unrecognized Name Attribute: " + nameAttr);
283 }
284 } else {
285 log.warn("Improperly formed Name Attribute: " + nameAttr);
286 }
287 }
288 }
289 } else {
290 log.error("Expected Attributes are missing.");
291 }
292
293 log.debug("Exiting SamlTokenExtractor.extractAttributeInfo...");
294 }
295
296 /**
297 * The value of the UserName attribute is assumed to be a user's name in
298 * plain text. The name parts are extracted in this method as the first
299 * word constitutes the first name, the last word constitutes the last name
300 * and all other text in between these words constitute the middle name.
301 * @param attrib The Attribute that has the user name as its value
302 * @param assertOut The Assertion element being written to
303 */
304 private static void extractNameParts(AttributeType attrib, AssertionType assertOut) {
305 log.debug("Entering SamlTokenExtractor.extractNameParts...");
306
307 // Assumption is that before the 1st space reflects the first name,
308 // after the last space is the last name, anything between is the middle name
309 List attrVals = attrib.getAttributeValue();
310 if (attrVals != null && !attrVals.isEmpty()) {
311 PersonNameType personName = new PersonNameType();
312 assertOut.getUserInfo().setPersonName(personName);
313
314 for (int idxVal = 0; idxVal <
315 attrVals.size(); idxVal++) {
316 if (attrVals.get(idxVal) != null) {
317 String completeName = attrVals.get(idxVal).toString();
318 String[] nameTokens = completeName.split("\\s");
319 ArrayList<String> nameParts = new ArrayList<String>();
320
321 //remove blank tokens
322 for (String tok : nameTokens) {
323 if (tok.trim() != null && tok.trim().length() > 0) {
324 nameParts.add(tok);
325 }
326 }
327
328 if (nameParts.size() > 0) {
329 if (!nameParts.get(0).isEmpty()) {
330 log.info("Setting User's Given Name to:" + nameParts.get(0));
331
332 personName.setGivenName(nameParts.get(0));
333 nameParts.remove(0);
334 }
335 }
336
337 if (nameParts.size() > 0) {
338 if (!nameParts.get(nameParts.size() - 1).isEmpty()) {
339 log.info("Setting User's Family Name to: " + nameParts.get(nameParts.size() - 1));
340 personName.setFamilyName(nameParts.get(nameParts.size() - 1));
341 nameParts.remove(nameParts.size() - 1);
342 }
343 }
344
345 if (nameParts.size() > 0) {
346 StringBuffer midName = new StringBuffer();
347 for (String name : nameParts) {
348 midName.append(name + " ");
349 }
350 // take off last blank character
351 midName.setLength(midName.length() - 1);
352 log.info("Setting User's Middle Name to: " + midName.toString());
353 personName.setSecondNameOrInitials(midName.toString());
354 }
355 // Once found break out of the loop
356 break;
357 }
358 }
359 } else {
360 log.error("User Name attribute is empty: " + attrVals);
361 }
362
363 log.debug("Token_Rcv.extractNameParts() -- End");
364 }
365
366 /**
367 * The value of the UserRole and PurposeForUse attributes are formatted
368 * according to the specifications of an nhin:CodedElement. This method
369 * parses that expected structure to obtain the code, codeSystem,
370 * codeSystemName, and the displayName attributes of that element.
371 * @param attrib The Attribute that has the UserRole or PurposeForUse as its
372 * value
373 * @param assertOut The Assertion element being written to
374 * @param id Identifies which coded element this is parsing
375 */
376 private static CeType parseRole(AttributeType attrib, String id) {
377 log.debug("Entering SamlTokenExtractor.parseRole...");
378
379 CeType ce = new CeType();
380
381 List attrVals = attrib.getAttributeValue();
382
383 if (attrVals != null && !attrVals.isEmpty()) {
384 for (int idxVal = 0; idxVal <
385 attrVals.size(); idxVal++) {
386 if (attrVals.get(idxVal) instanceof ElementNSImpl) {
387 ElementNSImpl elem = (ElementNSImpl) attrVals.get(idxVal);
388 NodeList nodelist = elem.getChildNodes();
389 for (int idx = 0; idx < nodelist.getLength(); idx++) {
390 if (nodelist.item(idx) instanceof Node) {
391 Node node = nodelist.item(idx);
392 NamedNodeMap attrMap = node.getAttributes();
393
394 for (int attrIdx = 0; attrIdx < attrMap.getLength(); attrIdx++) {
395 Node attrNode = attrMap.item(attrIdx);
396
397 if (attrNode != null &&
398 NullChecker.isNotNullish(attrNode.getNodeName())) {
399 if (attrNode.getNodeName().equalsIgnoreCase(CE_CODE_ID)) {
400 if (USER_ROLE_ID.equals(id)) {
401 log.info("Setting UserRole Code to: " + attrNode.getNodeValue());
402 ce.setCode(attrNode.getNodeValue());
403 } else if (PURPOSE_ROLE_ID.equals(id)) {
404 log.info("Setting Purpose Code to: " + attrNode.getNodeValue());
405 ce.setCode(attrNode.getNodeValue());
406 } else {
407 log.warn("Unrecognized item: " + id + " Cannot parse " + CE_CODE_ID);
408 }
409 }
410 if (attrNode.getNodeName().equalsIgnoreCase(CE_CODESYS_ID)) {
411 if (USER_ROLE_ID.equals(id)) {
412 log.info("Setting UserRode Code System to: " + attrNode.getNodeValue());
413 ce.setCodeSystem(attrNode.getNodeValue());
414 } else if (PURPOSE_ROLE_ID.equals(id)) {
415 log.info("Setting Purpose Code System to: " + attrNode.getNodeValue());
416 ce.setCodeSystem(attrNode.getNodeValue());
417
418 } else {
419 log.warn("Unrecognized item: " + id + " Cannot parse " + CE_CODESYS_ID);
420 }
421 }
422 if (attrNode.getNodeName().equalsIgnoreCase(CE_CODESYSNAME_ID)) {
423 if (USER_ROLE_ID.equals(id)) {
424 log.info("Setting UserRode Code System Name to: " + attrNode.getNodeValue());
425 ce.setCodeSystemName(attrNode.getNodeValue());
426 } else if (PURPOSE_ROLE_ID.equals(id)) {
427 log.info("Setting Purpose Code System Name to: " + attrNode.getNodeValue());
428 ce.setCodeSystemName(attrNode.getNodeValue());
429 } else {
430 log.warn("Unrecognized item: " + id + " Cannot parse " + CE_CODESYSNAME_ID);
431 }
432 }
433 if (attrNode.getNodeName().equalsIgnoreCase(CE_DISPLAYNAME_ID)) {
434 if (USER_ROLE_ID.equals(id)) {
435 log.info("Setting UserRode Display to: " + attrNode.getNodeValue());
436 ce.setDisplayName(attrNode.getNodeValue());
437 } else if (PURPOSE_ROLE_ID.equals(id)) {
438 log.info("Setting Purpose Display to: " + attrNode.getNodeValue());
439 ce.setDisplayName(attrNode.getNodeValue());
440 } else {
441 log.warn("Unrecognized item: " + id + " Cannot parse " + CE_DISPLAYNAME_ID);
442 }
443 }
444 }
445 }
446 }
447 }
448 } else {
449 log.warn("The value for the " + id + " attribute is not a proper AttributeValue.");
450 }
451 // Once found break out of the loop
452 break;
453 }
454 } else {
455 log.warn("Attributes for " + id + " are invalid: " + attrVals);
456 }
457
458 log.debug("Exiting SamlTokenExtractor.parseRole...");
459 return ce;
460 }
461
462 /**
463 * The Authorization Decision Statement is used to convey a form authorizing
464 * access to medical records. It may embed the binary content of the
465 * authorization form as well describing the conditions of its validity.
466 * This method saves off all values associated with this Evidence.
467 * @param authzState The authorization decision element
468 * @param assertOut The Assertion element being written to
469 */
470 private static void extractDecisionInfo(AuthzDecisionStatementType authzState, AssertionType assertOut) {
471 log.debug("Entering SamlTokenExtractor.extractDecisionInfo...");
472
473 EvidenceType evid = authzState.getEvidence();
474 List asserts = evid.getAssertionIDRefOrAssertionURIRefOrAssertion();
475 if (asserts != null && !asserts.isEmpty()) {
476 for (int idx = 0; idx <
477 asserts.size(); idx++) {
478 if (asserts.get(idx) instanceof JAXBElement) {
479 JAXBElement evElem = (JAXBElement) asserts.get(idx);
480 if (evElem.getValue() instanceof com.sun.xml.wss.saml.internal.saml20.jaxb20.AssertionType) {
481 com.sun.xml.wss.saml.internal.saml20.jaxb20.AssertionType evAss = (com.sun.xml.wss.saml.internal.saml20.jaxb20.AssertionType) evElem.getValue();
482
483 extractConditionsInfo(assertOut, evAss.getConditions());
484
485 List statements = evAss.getStatementOrAuthnStatementOrAuthzDecisionStatement();
486 if (statements != null && !statements.isEmpty()) {
487 for (int idxState = 0; idxState <
488 statements.size(); idxState++) {
489 if (statements.get(idxState) instanceof AttributeStatementType) {
490 AttributeStatementType statement = (AttributeStatementType) statements.get(idxState);
491 extractAttributeInfo(statement, assertOut);
492 }
493 }
494 } else {
495 log.error("Evidence Statements are missing.");
496 }
497 } else {
498 log.warn("Non-Assertion type element: " + evElem.getValue() + " is not processed");
499 }
500 } else {
501 log.error("Evidence assertion is not recognized as a JAXB element");
502 }
503 }
504 } else {
505 log.error("Evidence assertion is empty: " + asserts);
506 }
507 log.debug("Exiting SamlTokenExtractor.extractDecisionInfo...");
508 }
509
510 /**
511 * This method extracts the dates of validity for the Evidence's
512 * authorization form. These dates are contained in the Conditions element
513 * and are written out to the storage file by this method.
514 * @param assertOut The Assertion element being written to
515 * @param conditions The Evidence's Conditions element
516 */
517 private static void extractConditionsInfo(AssertionType assertOut, ConditionsType conditions) {
518 log.debug("Entering SamlTokenExtractor.extractConditionsInfo...");
519
520 if (conditions != null) {
521 SimpleDateFormat dateForm = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
522
523 XMLGregorianCalendar beginTime = conditions.getNotBefore();
524 if (beginTime != null && beginTime.toGregorianCalendar() != null && beginTime.toGregorianCalendar().getTime() != null) {
525 String formBegin = dateForm.format(beginTime.toGregorianCalendar().getTime());
526
527 if (NullChecker.isNotNullish(formBegin)) {
528 log.info("Setting Signature Date to: " + formBegin);
529 assertOut.setDateOfSignature(formBegin);
530 }
531 }
532
533 XMLGregorianCalendar endTime = conditions.getNotOnOrAfter();
534 if (endTime != null && endTime.toGregorianCalendar() != null && endTime.toGregorianCalendar().getTime() != null) {
535 String formEnd = dateForm.format(endTime.toGregorianCalendar().getTime());
536
537 if (NullChecker.isNotNullish(formEnd)) {
538 log.info("Setting Expires Date to: " + formEnd);
539 assertOut.setExpirationDate(formEnd);
540 }
541 }
542 }
543 log.debug("Exiting SamlTokenExtractor.extractConditionsInfo...");
544 }
545}
Note: See TracBrowser for help on using the repository browser.